From 2f7196e2f7665b331186f997d49448f1849805b9 Mon Sep 17 00:00:00 2001 From: Branden Bonaby <105318831+bbonaby@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:48:19 -0700 Subject: [PATCH 01/10] Add scoped PSEC 1.0 runtime proxy loopback workaround Retain explicit host-loopback opt-in and PSEC 1.1+ support-flag checks. Add compatibility gate tests and target-host proxy assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d91c468f-e7d6-4cfd-a21d-d01f30f2ae69 --- .../common/src/base_container_runner.rs | 197 +++++++++++++++++- ...un_processcontainer_network_proxy_test.ps1 | 12 +- 2 files changed, 197 insertions(+), 12 deletions(-) diff --git a/src/backends/process_container/common/src/base_container_runner.rs b/src/backends/process_container/common/src/base_container_runner.rs index 29947eb79..c454254d8 100644 --- a/src/backends/process_container/common/src/base_container_runner.rs +++ b/src/backends/process_container/common/src/base_container_runner.rs @@ -248,7 +248,7 @@ impl BaseContainerRunner { #[cfg(test)] fn build_process_security_environment_spec(request: &ExecutionRequest) -> Vec { - let version = Self::choose_min_required_psec_version_for_request(request); + let version = Self::choose_min_required_psec_version_for_request(request, false); build_psec_v1_security_environment_spec( request, version, @@ -316,10 +316,36 @@ impl BaseContainerRunner { && secenv::query_support(SecurityEnvironmentSupport::NetworkIngress) } + // Temporary compatibility for explicit identity-less proxies on PSEC 1.0-only + // hosts. Remove once those hosts support PSEC 1.1; keep loopback allow opt-in. + fn psec_1_0_proxy_loopback_workaround( + request: &ExecutionRequest, + mut supports_version: impl FnMut( + SecurityEnvironmentVersion, + ) + -> Result, + ) -> Result { + let policy = &request.policy; + if !policy.runtime_network_proxy_specified + || !policy.network_proxy.is_enabled() + || policy.allowed_proxy_peer.is_some() + || !unrestricted_host_loopback_allowed(policy) + { + return Ok(false); + } + // A 1.1+ host must keep using the advertised ingress support flag, + // even if it also accepts a 1.0 payload. + if supports_version(SecurityEnvironmentVersion::V1_1)? { + return Ok(false); + } + supports_version(SecurityEnvironmentVersion::V1_0) + } + /// The single PSEC version-selection point: start at 1.0 and raise it to /// the highest version required by any requested native feature. fn choose_min_required_psec_version_for_request( request: &ExecutionRequest, + proxy_loopback_workaround: bool, ) -> SecurityEnvironmentVersion { let mut version = SecurityEnvironmentVersion::V1_0; if !request.policy.denied_paths.is_empty() { @@ -329,7 +355,7 @@ impl BaseContainerRunner { version = version.max(SecurityEnvironmentSupport::FileSystemEnumerate.required_version()); } - if unrestricted_host_loopback_allowed(&request.policy) { + if unrestricted_host_loopback_allowed(&request.policy) && !proxy_loopback_workaround { version = version.max(SecurityEnvironmentSupport::NetworkIngress.required_version()); } version @@ -374,7 +400,10 @@ impl BaseContainerRunner { if unrestricted_host_loopback_allowed(&request.policy) && !Self::supports_ingress_host_loopback_allow() { - return BaseContainerRequestDecision::IngressUnsupported; + match Self::psec_1_0_proxy_loopback_workaround(request, secenv::supports_version) { + Ok(true) => {} + Ok(false) | Err(_) => return BaseContainerRequestDecision::IngressUnsupported, + } } BaseContainerRequestDecision::Serviceable } @@ -405,7 +434,24 @@ impl BaseContainerRunner { logger, ); - let psec_version = Self::choose_min_required_psec_version_for_request(request); + let proxy_loopback_workaround = + Self::psec_1_0_proxy_loopback_workaround(request, secenv::supports_version).map_err( + |error| ScriptResponse { + failure_phase: FailurePhase::BackendUnavailable, + ..ScriptResponse::error(&format!( + "failed to query PSEC proxy compatibility support: {error}" + )) + }, + )?; + if proxy_loopback_workaround { + let _ = writeln!( + logger, + "warning: using PSEC 1.0 proxy loopback compatibility; the proxy is \ + identified only by its endpoint, not by its host process identity" + ); + } + let psec_version = + Self::choose_min_required_psec_version_for_request(request, proxy_loopback_workaround); let version_supported = secenv::supports_version(psec_version).map_err(|error| ScriptResponse { failure_phase: FailurePhase::BackendUnavailable, @@ -2836,7 +2882,10 @@ mod tests { #[test] fn required_psec_contract_uses_v1_1_for_enumeration() { assert_eq!( - BaseContainerRunner::choose_min_required_psec_version_for_request(&enumerate_request()), + BaseContainerRunner::choose_min_required_psec_version_for_request( + &enumerate_request(), + false + ), SecurityEnvironmentVersion::V1_1 ); } @@ -2845,7 +2894,8 @@ mod tests { fn required_psec_contract_uses_v1_1_for_host_loopback() { assert_eq!( BaseContainerRunner::choose_min_required_psec_version_for_request( - &host_loopback_request() + &host_loopback_request(), + false ), SecurityEnvironmentVersion::V1_1 ); @@ -2855,12 +2905,145 @@ mod tests { fn required_psec_contract_defaults_to_v1_0() { assert_eq!( BaseContainerRunner::choose_min_required_psec_version_for_request( - &ExecutionRequest::default() + &ExecutionRequest::default(), + false ), SecurityEnvironmentVersion::V1_0 ); } + fn psec_1_0_proxy_request() -> ExecutionRequest { + let mut request = host_loopback_request(); + request.policy.runtime_network_proxy_specified = true; + request.policy.network_proxy = ProxyConfig { + address: Some(ProxyAddress::new("127.0.0.1".to_string(), 8080)), + builtin_test_server: false, + }; + request + } + + #[test] + fn psec_1_0_proxy_workaround_requires_a_1_0_only_host() { + let request = psec_1_0_proxy_request(); + for (supports_1_0, supports_1_1, expected) in [ + (true, false, true), + (true, true, false), + (false, false, false), + ] { + let mut queried = Vec::new(); + let workaround = + BaseContainerRunner::psec_1_0_proxy_loopback_workaround(&request, |version| { + queried.push(version); + Ok(if version == SecurityEnvironmentVersion::V1_1 { + supports_1_1 + } else { + supports_1_0 + }) + }) + .unwrap(); + assert_eq!(workaround, expected); + assert_eq!( + queried, + if supports_1_1 { + vec![SecurityEnvironmentVersion::V1_1] + } else { + vec![ + SecurityEnvironmentVersion::V1_1, + SecurityEnvironmentVersion::V1_0, + ] + } + ); + } + } + + #[test] + fn psec_1_0_proxy_workaround_requires_explicit_identity_less_proxy_and_loopback_allow() { + for case in 0..4 { + let mut request = psec_1_0_proxy_request(); + match case { + 0 => request.policy.runtime_network_proxy_specified = false, + 1 => request.policy.network_proxy = ProxyConfig::default(), + 2 => request.policy.allowed_proxy_peer = Some("Contoso.Proxy_123".to_string()), + 3 => { + request + .policy + .network_ingress + .as_mut() + .unwrap() + .host_loopback = NetworkAction::Deny; + } + _ => unreachable!(), + } + assert!(!BaseContainerRunner::psec_1_0_proxy_loopback_workaround( + &request, + |_| panic!("ineligible policy must not query the OS"), + ) + .unwrap()); + } + } + + #[test] + fn psec_1_0_proxy_workaround_propagates_version_query_errors() { + let request = psec_1_0_proxy_request(); + for failing_version in [ + SecurityEnvironmentVersion::V1_0, + SecurityEnvironmentVersion::V1_1, + ] { + let result = + BaseContainerRunner::psec_1_0_proxy_loopback_workaround(&request, |version| { + if version == failing_version { + Err(learning_mode_windows::LearningModeError::HResultCall { + function: "IsProcessSecurityEnvironmentVersionSupported", + code: E_NOTIMPL.0, + }) + } else { + Ok(false) + } + }); + assert!(matches!( + result, + Err(learning_mode_windows::LearningModeError::HResultCall { .. }) + )); + } + } + + #[test] + fn psec_1_0_proxy_workaround_encodes_existing_loopback_capability_and_peer() { + let request = psec_1_0_proxy_request(); + let version = + BaseContainerRunner::choose_min_required_psec_version_for_request(&request, true); + assert_eq!(version, SecurityEnvironmentVersion::V1_0); + let bytes = build_psec_v1_security_environment_spec(&request, version, false); + let spec = psec_layout::root_as_process_security_environment(&bytes).unwrap(); + let network = spec.network_policy().unwrap(); + assert_eq!(spec.version().minor(), 0); + assert!(spec + .capabilities() + .unwrap() + .split(',') + .any(|capability| capability == "networkLoopback")); + assert_eq!( + network.allowed_appcontainer_peer(), + Some(crate::base_container_helpers::LOOPBACK_NETWORK_PEER) + ); + assert_eq!( + network.proxy().unwrap().url(), + Some("http://127.0.0.1:8080") + ); + assert!(network.ingress().is_none()); + assert!(network.egress().is_none()); + } + + #[test] + fn psec_1_0_proxy_workaround_does_not_lower_enumeration_requirement() { + let mut request = psec_1_0_proxy_request(); + request.policy.enumerate_paths = vec!["C:\\tools".to_string()]; + assert_eq!( + BaseContainerRunner::choose_min_required_psec_version_for_request(&request, true), + SecurityEnvironmentVersion::V1_1 + ); + } + #[test] #[should_panic( expected = "build_psec_v1_security_environment_spec only supports PSEC major version 1" diff --git a/tests/scripts/run_processcontainer_network_proxy_test.ps1 b/tests/scripts/run_processcontainer_network_proxy_test.ps1 index 44c975b29..d5695a8f1 100644 --- a/tests/scripts/run_processcontainer_network_proxy_test.ps1 +++ b/tests/scripts/run_processcontainer_network_proxy_test.ps1 @@ -87,15 +87,18 @@ function Invoke-NetworkProxyAssertions { if ($psec) { $out = $envRun.Result.Stdout $ran = [bool]($out -match '(?im)^SystemRoot=') - # networking.md: hostLoopback=allow needs the PSEC 1.1 ingress contract - # and "is rejected when the PSEC 1.1 ingress contract is unavailable". - # Only two outcomes are documented, so a bare OS error is a failure even - # on a 1.0-only host -- the caller cannot act on E_INVALIDARG. + # PSEC 1.0-only hosts use the explicit identity-less proxy workaround. + # PSEC 1.1+ still requires the ingress support flag. A bare OS error + # is never a valid substitute for successful execution or rejection. $rejectedCleanly = Test-WasRejected $envRun Record-Result -Phase 'P8e' -Name 'identity-less proxy config either runs or is rejected cleanly (never a bare OS error)' ` -Pass ($ran -or $rejectedCleanly) ` -Detail ("ran=$ran; rejectedAtValidation=$rejectedCleanly; exit=$($envRun.Result.ExitCode); " + "stderr=$(Format-Snippet $envRun.Result.Stderr)") + if ($envRun.Log -match 'using PSEC 1\.0 proxy loopback compatibility') { + Record-Result -Phase 'P8e' -Name 'PSEC 1.0 proxy workaround launches the workload' ` + -Pass $ran -Detail "exit=$($envRun.Result.ExitCode); ran=$ran" + } if ($ran) { foreach ($v in @('HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy')) { # cmd.exe `set` upper-cases nothing, but Windows env lookup is @@ -223,4 +226,3 @@ function Invoke-NetworkProxyAssertions { Invoke-WpcPhase -Key 'NetworkProxy' -Body { Phase-NetworkProxy } Complete-WpcChild - From 381835f440f45e332f953285a1b9119878b4524c Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 11:55:22 -0300 Subject: [PATCH 02/10] feat(probe): expose PSEC proxy loopback compatibility Report the PSEC 1.0-only proxy workaround through native discovery and the Rust, Node, and .NET V1 probe surfaces. Share the execution version gate, retain diagnostics, and document the narrow policy requirements. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671f3dd1-048a-4b89-acb6-e762eae992af Signed-off-by: Carlos Alexandro Becker --- docs/process-container/networking.md | 27 +++++- docs/reference/dotnet/v1/types.md | 6 ++ docs/reference/node/v1/types.md | 6 ++ docs/reference/rust/v1/types.md | 6 ++ .../Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs | 11 ++- .../V1/MxcContainerTests.cs | 37 ++++++++ .../Microsoft.Mxc.Sdk/MxcJsonSerialization.cs | 1 + .../Microsoft.Mxc.Sdk/V1/MxcContainer.cs | 2 + .../Microsoft.Mxc.Sdk/V1/MxcPlatform.cs | 1 + .../Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs | 12 +++ sdk/dotnet/README.md | 6 ++ sdk/node/README.md | 6 ++ sdk/node/src/v1/platform.ts | 1 + sdk/node/src/v1/probe.ts | 1 + sdk/node/src/v1/types.ts | 3 + sdk/node/tests/unit/platform.test.ts | 13 +++ sdk/node/tests/unit/probe.test.ts | 29 ++++++ src/ffi/mxc_ffi/tests/ffi.rs | 1 + src/mxc-sdk/README.md | 5 + .../common/base_container_runner.rs | 83 ++++++++++++----- .../process_container/common/probe.rs | 25 ++++- src/mxc-sdk/src/core/mxc_engine/probe.rs | 93 ++++++++++++++++--- 22 files changed, 333 insertions(+), 42 deletions(-) diff --git a/docs/process-container/networking.md b/docs/process-container/networking.md index 3320b40f4..198aa02ab 100644 --- a/docs/process-container/networking.md +++ b/docs/process-container/networking.md @@ -114,11 +114,32 @@ PSEC rejects it alongside a proxy. Their ingress default still selects `privateN host-loopback setting selects both the `networkLoopback` capability and the `MXC-Loopback` peer rather than an identity-scoped proxy peer. The peer alone is insufficient: without the capability, the environment can be created but its client cannot reach the host proxy. -The public ingress policy remains required, and host-loopback allow retains the PSEC 1.1 host requirement. -Requests that do not allow host loopback use the -PSEC 1.0 capability mapping; `hostLoopback: "allow"` is rejected when the PSEC 1.1 ingress contract is unavailable. +The public ingress policy remains required. Direct-egress host-loopback allow +requires PSEC 1.1 and the ingress support flag. For an identity-less +`runtimeConfig.networkProxy` with explicit `hostLoopback: "allow"`, MXC also +supports a compatibility workaround on hosts that support PSEC 1.0 but not 1.1. +Hosts that support PSEC 1.1 or newer must still advertise the ingress support +flag; accepting a 1.0 payload does not enable this workaround. +Requests that do not allow host loopback use the PSEC 1.0 capability mapping. Requests the PSEC contract cannot preserve continue to an AppContainer tier. +#### Discovering proxy loopback compatibility + +Backend discovery reports `proxyLoopbackCompatibility` on the ProcessContainer +`base-container` tier only when the PSEC 1.0-only workaround is available. +The Windows request probe also reports +`probes.baseContainerSupportsProxyLoopbackCompatibility`, including when called +without a request. Both use the same PSEC version gate as execution. A query +failure omits the capability, sets the probe fact to `false`, and adds a warning. + +This does not grant general `ingressHostLoopbackAllow` support or report that the +current request uses the workaround. The request must set +`runtimeConfig.networkProxy`, omit `processContainer.network.allowedProxyPeer`, +and explicitly allow host loopback, while retaining the Model 2 network policy. +Other requested features still require their own host support. +Use the request-aware probe for the complete policy. Neither probe launches the +workload or checks proxy reachability. + **Unresolved ingress limitation:** on Windows build 26691.1002, this proxy mapping permits unprivileged connections to the configured proxy and blocks direct loopback bypasses, but host-to-container listener connections still time out. The same listener succeeds under a non-proxy PSEC ingress policy. The proxy mapping is therefore not yet a diff --git a/docs/reference/dotnet/v1/types.md b/docs/reference/dotnet/v1/types.md index 6c024f8b6..a667671ae 100644 --- a/docs/reference/dotnet/v1/types.md +++ b/docs/reference/dotnet/v1/types.md @@ -36,9 +36,14 @@ public enum BackendCapability FilesystemDeniedPaths = 3, IngressHostLoopbackAllow = 4, FilesystemEnumeratePaths = 5, + ProxyLoopbackCompatibility = 6, } ``` +`ProxyLoopbackCompatibility` reports the identity-less proxy workaround on +PSEC 1.0-only hosts, not general ingress support. See +[proxy compatibility discovery](../../../process-container/networking.md#discovering-proxy-loopback-compatibility). + ## `Microsoft.Mxc.Sdk.V1.CaptureDenialsError` @@ -952,6 +957,7 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } + public bool BaseContainerSupportsProxyLoopbackCompatibility { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } diff --git a/docs/reference/node/v1/types.md b/docs/reference/node/v1/types.md index a15456728..4ea474033 100644 --- a/docs/reference/node/v1/types.md +++ b/docs/reference/node/v1/types.md @@ -29,9 +29,14 @@ export type BackendCapability = | 'filesystemEnumeratePaths' | 'ingressHostLoopbackAllow' | 'proxyEnforcement' + | 'proxyLoopbackCompatibility' | 'unknown'; ``` +`proxyLoopbackCompatibility` reports the identity-less proxy workaround on +PSEC 1.0-only hosts, not general ingress support. See +[proxy compatibility discovery](../../../process-container/networking.md#discovering-proxy-loopback-compatibility). + ## `@microsoft/mxc-sdk/v1::BaseProcessUiConfig` BaseProcess-specific UI configuration (Windows only). @@ -576,6 +581,7 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; + baseContainerSupportsProxyLoopbackCompatibility: boolean; isolationSessionAvailable: boolean; hyperlightAvailable: boolean; uiCapabilities: UiCapabilitySupport; diff --git a/docs/reference/rust/v1/types.md b/docs/reference/rust/v1/types.md index 7369705dc..cc2aea43c 100644 --- a/docs/reference/rust/v1/types.md +++ b/docs/reference/rust/v1/types.md @@ -29,9 +29,14 @@ pub enum BackendCapability { FilesystemEnumeratePaths, IngressHostLoopbackAllow, ProxyEnforcement, + ProxyLoopbackCompatibility, } ``` +`ProxyLoopbackCompatibility` reports the identity-less proxy workaround on +PSEC 1.0-only hosts, not general ingress support. See +[proxy compatibility discovery](../../../process-container/networking.md#discovering-proxy-loopback-compatibility). + ## `mxc_sdk::v1::BubblewrapNetworkSupport` @@ -533,6 +538,7 @@ pub struct ProbeFacts { pub base_container_supports_deny_paths: bool, pub base_container_supports_enumerate_paths: bool, pub base_container_supports_ingress_host_loopback_allow: bool, + pub base_container_supports_proxy_loopback_compatibility: bool, pub isolation_session_available: bool, pub hyperlight_available: bool, pub ui_capabilities: UiCapabilitySupport, diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs index 5fe7cfd67..5b239221c 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs @@ -337,7 +337,8 @@ static void CheckRoundTrip(T value, Func check, string name) "bfsCompiledIn": false, "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, - "baseContainerSupportsIngressHostLoopbackAllow": true, + "baseContainerSupportsIngressHostLoopbackAllow": false, + "baseContainerSupportsProxyLoopbackCompatibility": true, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -359,6 +360,14 @@ static void CheckRoundTrip(T value, Func check, string name) Check(probe.Tier == IsolationTier.AppContainerDacl, "probe tier parsed"); Check(probe.NeedsDaclAugmentation == true, "probe dacl augmentation parsed"); Check(probe.Probes.BaseContainerApiPresent, "probe facts parsed"); +Check(probe.Probes.BaseContainerSupportsProxyLoopbackCompatibility, "proxy loopback compatibility parsed"); Check(probe.Probes.UiCapabilities.CanBlockClipboardRead, "probe ui capabilities parsed"); +var nativeProbe = MxcJson.Deserialize(probeJson, MxcJson.ProbeOptions)!; +CheckRoundTrip(nativeProbe.Probes!, + x => x.BaseContainerSupportsProxyLoopbackCompatibility, "proxy loopback compatibility facts"); +var discovery = MxcPlatform.ParseAvailableBackends( + """[{"backend":"processcontainer","tier":"base-container","capabilities":["proxyLoopbackCompatibility"]}]"""); +Check(discovery.Single().Capabilities.SequenceEqual([BackendCapability.ProxyLoopbackCompatibility]), + "proxy loopback compatibility discovery"); Console.WriteLine("AOT smoke test passed: all JSON paths are reflection-free."); diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs index 01fafa729..45ac4e408 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs @@ -30,6 +30,7 @@ public class MxcContainerTests "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, "baseContainerSupportsIngressHostLoopbackAllow": true, + "baseContainerSupportsProxyLoopbackCompatibility": false, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -272,6 +273,40 @@ public void ProbeParser_RejectsMissingRequiredProbeBoolean() json["probes"]!.AsObject().Remove("bfscfgPresent")); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ProbeParser_PreservesProxyLoopbackCompatibility(bool supported) + { + var json = CreateCompleteProbeJson(); + json["tier"] = "base-container"; + json["needsDaclAugmentation"] = false; + json["probes"]!["baseContainerSupportsIngressHostLoopbackAllow"] = false; + json["probes"]!["baseContainerSupportsProxyLoopbackCompatibility"] = supported; + + var output = MxcContainer.ParseProbeOutput(json.ToJsonString()); + + Assert.Equal(supported, output.Probes.BaseContainerSupportsProxyLoopbackCompatibility); + Assert.False(output.Probes.BaseContainerSupportsIngressHostLoopbackAllow); + } + + [Fact] + public void ProbeParser_RejectsMissingProxyLoopbackCompatibility() + { + AssertProbeJsonRejected(json => + json["probes"]!.AsObject().Remove("baseContainerSupportsProxyLoopbackCompatibility")); + } + + [Theory] + [InlineData("null")] + [InlineData("\"true\"")] + [InlineData("1")] + public void ProbeParser_RejectsInvalidProxyLoopbackCompatibility(string value) + { + AssertProbeJsonRejected(json => + json["probes"]!["baseContainerSupportsProxyLoopbackCompatibility"] = JsonNode.Parse(value)); + } + [Fact] public void ProbeParser_RejectsMissingRequiredUiBoolean() { @@ -456,6 +491,7 @@ public void Discovery_MapsEveryNativeIsolationTier( [InlineData("filesystemEnumeratePaths", BackendCapability.FilesystemEnumeratePaths)] [InlineData("ingressHostLoopbackAllow", BackendCapability.IngressHostLoopbackAllow)] [InlineData("proxyEnforcement", BackendCapability.ProxyEnforcement)] + [InlineData("proxyLoopbackCompatibility", BackendCapability.ProxyLoopbackCompatibility)] public void Discovery_MapsEveryNativeCapability( string wireName, BackendCapability expected) @@ -472,6 +508,7 @@ public void BackendCapability_PreservesReleasedNumericValues() Assert.Equal(3, (int)BackendCapability.FilesystemDeniedPaths); Assert.Equal(4, (int)BackendCapability.IngressHostLoopbackAllow); Assert.Equal(5, (int)BackendCapability.FilesystemEnumeratePaths); + Assert.Equal(6, (int)BackendCapability.ProxyLoopbackCompatibility); } [Theory] diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs index beb2b5147..d6eb9acf0 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs @@ -82,6 +82,7 @@ public CamelCaseJsonStringEnumConverter() [JsonSerializable(typeof(NativeAvailableBackend[]))] [JsonSerializable(typeof(NativePlatformSupport))] [JsonSerializable(typeof(NativeProbeOutput))] +[JsonSerializable(typeof(NativeProbeFacts))] [JsonSerializable(typeof(ExecutionMetadata))] [JsonSerializable(typeof(CaptureDenialsResult))] [JsonSerializable(typeof(CaptureDenialsError))] diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs index 07b37d411..c1e6a06a0 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs @@ -177,6 +177,8 @@ internal static ProbeOutput ParseProbeOutput(string json) probes.BaseContainerSupportsEnumeratePaths, BaseContainerSupportsIngressHostLoopbackAllow = probes.BaseContainerSupportsIngressHostLoopbackAllow, + BaseContainerSupportsProxyLoopbackCompatibility = + probes.BaseContainerSupportsProxyLoopbackCompatibility, IsolationSessionAvailable = probes.IsolationSessionAvailable, HyperlightAvailable = probes.HyperlightAvailable, UiCapabilities = new UiCapabilitySupport diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs index b61bb9885..2a6df8549 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs @@ -118,6 +118,7 @@ internal static BackendCapability ParseBackendCapability(string value) => "filesystemEnumeratePaths" => BackendCapability.FilesystemEnumeratePaths, "ingressHostLoopbackAllow" => BackendCapability.IngressHostLoopbackAllow, "proxyEnforcement" => BackendCapability.ProxyEnforcement, + "proxyLoopbackCompatibility" => BackendCapability.ProxyLoopbackCompatibility, _ => BackendCapability.Unknown, }; diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs index 640d81657..7f38bc310 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs @@ -78,6 +78,12 @@ public enum BackendCapability /// Native filesystem enumeration-only access at the reported tier. /// FilesystemEnumeratePaths = 5, + + /// + /// Identity-less proxy loopback workaround on PSEC 1.0-only hosts. + /// Requires explicit host-loopback allow; not general ingress support. + /// + ProxyLoopbackCompatibility = 6, } /// One host-available backend and its probed capabilities. @@ -146,6 +152,8 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } + /// PSEC 1.0-only identity-less proxy workaround, not general ingress support. + public bool BaseContainerSupportsProxyLoopbackCompatibility { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } @@ -279,6 +287,10 @@ internal sealed class NativeProbeFacts [JsonPropertyName("baseContainerSupportsIngressHostLoopbackAllow")] public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } + [JsonRequired] + [JsonPropertyName("baseContainerSupportsProxyLoopbackCompatibility")] + public bool BaseContainerSupportsProxyLoopbackCompatibility { get; init; } + [JsonRequired] [JsonPropertyName("isolationSessionAvailable")] public bool IsolationSessionAvailable { get; init; } diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md index 44f7ad0be..9e117e8b3 100644 --- a/sdk/dotnet/README.md +++ b/sdk/dotnet/README.md @@ -235,6 +235,12 @@ sandbox on the current host. `MxcPlatform.GetAvailableBackends()` reports host backend capabilities; availability is advisory and launch-time validation still applies. +For the PSEC 1.0-only identity-less proxy workaround, check +`BackendCapability.ProxyLoopbackCompatibility` or, on Windows, +`MxcContainer.Probe().Probes.BaseContainerSupportsProxyLoopbackCompatibility`. +This is not general host-loopback ingress support; see +[proxy compatibility discovery](../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). + Creation telemetry is supplied through `Telemetry` on `RunOptions`, `SpawnOptions`, or `SpawnWithPtyOptions`, not on `ContainerRequest`. Omission leaves telemetry disabled; `new TelemetryConfig { Enabled = false }` diff --git a/sdk/node/README.md b/sdk/node/README.md index b3e7e9b21..f2b6a1e3a 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -254,6 +254,12 @@ through V1 creation. Discovery is advisory, and launch-time validation still applies. Native failures and malformed discovery results throw rather than reporting an unsupported host. +For the PSEC 1.0-only identity-less proxy workaround, check the ProcessContainer +capability `proxyLoopbackCompatibility` or, on Windows, +`probe().probes.baseContainerSupportsProxyLoopbackCompatibility`. +This is not general host-loopback ingress support; see +[proxy compatibility discovery](../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). + Filesystem discovery helpers and their result/options types are grouped under `policy.filesystem` from `@microsoft/mxc-sdk/v1`. They take an optional `environment` map; omission uses diff --git a/sdk/node/src/v1/platform.ts b/sdk/node/src/v1/platform.ts index 00687f4b6..610c9e2b0 100644 --- a/sdk/node/src/v1/platform.ts +++ b/sdk/node/src/v1/platform.ts @@ -26,6 +26,7 @@ const discoveryBackends: readonly ContainmentBackend[] = [ const discoveryCapabilities: readonly BackendCapability[] = [ 'captureDenials', 'filesystemDeniedPaths', 'filesystemEnumeratePaths', 'ingressHostLoopbackAllow', 'proxyEnforcement', + 'proxyLoopbackCompatibility', ]; const discoveryTiers: readonly IsolationTier[] = [ 'base-container', 'appcontainer-bfs', 'appcontainer-dacl', diff --git a/sdk/node/src/v1/probe.ts b/sdk/node/src/v1/probe.ts index b6b7b9a34..a6b7a296d 100644 --- a/sdk/node/src/v1/probe.ts +++ b/sdk/node/src/v1/probe.ts @@ -31,6 +31,7 @@ const probeFactBooleanFields: readonly (keyof ProbeFacts)[] = [ 'baseContainerSupportsDenyPaths', 'baseContainerSupportsEnumeratePaths', 'baseContainerSupportsIngressHostLoopbackAllow', + 'baseContainerSupportsProxyLoopbackCompatibility', 'isolationSessionAvailable', 'hyperlightAvailable', ]; diff --git a/sdk/node/src/v1/types.ts b/sdk/node/src/v1/types.ts index 03ca908c0..6bde66bf3 100644 --- a/sdk/node/src/v1/types.ts +++ b/sdk/node/src/v1/types.ts @@ -711,6 +711,7 @@ export type BackendCapability = | 'filesystemEnumeratePaths' | 'ingressHostLoopbackAllow' | 'proxyEnforcement' + | 'proxyLoopbackCompatibility' | 'unknown'; /** One host-available backend and its native capabilities. */ @@ -784,6 +785,8 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; + /** PSEC 1.0-only identity-less proxy workaround, not general ingress support. */ + baseContainerSupportsProxyLoopbackCompatibility: boolean; /** True when this executor includes IsolationSession and the host can activate it. */ isolationSessionAvailable: boolean; /** True when this executor includes Hyperlight and its host runtime is available. */ diff --git a/sdk/node/tests/unit/platform.test.ts b/sdk/node/tests/unit/platform.test.ts index f43ad1440..cb98a53f8 100644 --- a/sdk/node/tests/unit/platform.test.ts +++ b/sdk/node/tests/unit/platform.test.ts @@ -71,6 +71,19 @@ describe('native backend discovery', () => { }]); }); + it('reports proxy loopback compatibility without general ingress support', () => { + assert.deepStrictEqual(parseAvailableBackends(JSON.stringify([{ + backend: 'processcontainer', + tier: 'base-container', + capabilities: ['proxyLoopbackCompatibility'], + }])), [{ + backend: 'processcontainer', + tier: 'base-container', + capabilities: ['proxyLoopbackCompatibility'], + warnings: [], + }]); + }); + it('rejects malformed discovery results instead of returning no backends', () => { for (const payload of [ null, {}, [null], [[]], [{}], [{ backend: 1 }], diff --git a/sdk/node/tests/unit/probe.test.ts b/sdk/node/tests/unit/probe.test.ts index ba1d0b856..61d45f761 100644 --- a/sdk/node/tests/unit/probe.test.ts +++ b/sdk/node/tests/unit/probe.test.ts @@ -30,6 +30,7 @@ const completeProbe = { baseContainerSupportsDenyPaths: false, baseContainerSupportsEnumeratePaths: false, baseContainerSupportsIngressHostLoopbackAllow: false, + baseContainerSupportsProxyLoopbackCompatibility: false, isolationSessionAvailable: false, hyperlightAvailable: false, uiCapabilities: { @@ -192,6 +193,34 @@ describe('probe', () => { assert.equal(output.needsDaclAugmentation, true); }); + it('preserves proxy loopback compatibility independently of general ingress', () => { + for (const supported of [false, true]) { + const output = runProbeOutput({ + ...completeProbe, + tier: 'base-container', + needsDaclAugmentation: false, + probes: { + ...completeProbe.probes, + baseContainerSupportsProxyLoopbackCompatibility: supported, + }, + }); + assert.equal(output.probes.baseContainerSupportsProxyLoopbackCompatibility, supported); + assert.equal(output.probes.baseContainerSupportsIngressHostLoopbackAllow, false); + } + }); + + it('rejects missing or invalid proxy loopback compatibility facts', () => { + for (const value of [undefined, null, 'true', 1]) { + assertProbeOutputRejected({ + ...completeProbe, + probes: { + ...completeProbe.probes, + baseContainerSupportsProxyLoopbackCompatibility: value, + }, + }); + } + }); + it('rejects an unknown envelope field', () => { const payload = cloneCompleteProbe(); payload.unknownEnvelopeField = true; diff --git a/src/ffi/mxc_ffi/tests/ffi.rs b/src/ffi/mxc_ffi/tests/ffi.rs index a85886aa3..905e76845 100644 --- a/src/ffi/mxc_ffi/tests/ffi.rs +++ b/src/ffi/mxc_ffi/tests/ffi.rs @@ -78,6 +78,7 @@ fn extern_discovery_returns_owned_json() { | "filesystemEnumeratePaths" | "ingressHostLoopbackAllow" | "proxyEnforcement" + | "proxyLoopbackCompatibility" ) )); } diff --git a/src/mxc-sdk/README.md b/src/mxc-sdk/README.md index bc223c2bb..5852eb89d 100644 --- a/src/mxc-sdk/README.md +++ b/src/mxc-sdk/README.md @@ -141,6 +141,11 @@ the terminal open. Ordinary `spawn` continues to use separate pipes. For host discovery, use `mxc_sdk::v1::platform_support` and `mxc_sdk::v1::available_backends`. Errors are returned as `mxc_sdk::v1::Error` with an `ErrorCode`. +For the PSEC 1.0-only identity-less proxy workaround, check +`v1::BackendCapability::ProxyLoopbackCompatibility` or, on Windows, +`v1::probe(None)?.probes.base_container_supports_proxy_loopback_compatibility`. +This is not general host-loopback ingress support; see +[proxy compatibility discovery](../../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). Telemetry and policy helpers are also under `v1`. The [launch-choice table](../../../docs/reference/rust/v1/api.md#choosing-a-launch-operation) compares captured, piped, and terminal execution. diff --git a/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs b/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs index cbefde7a4..04d24ca05 100644 --- a/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs +++ b/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs @@ -14,7 +14,7 @@ use std::ptr; use std::sync::Arc; use crate::learning_mode_core::DenialAnalyzer; -use crate::learning_mode_windows::{EtlDenialAnalyzer, LEARNING_MODE_API_SET}; +use crate::learning_mode_windows::{EtlDenialAnalyzer, LearningModeError, LEARNING_MODE_API_SET}; use windows::Win32::Foundation::{ CloseHandle, GetLastError, SetHandleInformation, ERROR_CALL_NOT_IMPLEMENTED, E_NOTIMPL, HANDLE, HANDLE_FLAG_INHERIT, WAIT_OBJECT_0, WAIT_TIMEOUT, @@ -328,15 +328,32 @@ impl BaseContainerRunner { && secenv::query_support(SecurityEnvironmentSupport::NetworkIngress) } + /// Whether this host supports the identity-less proxy loopback workaround. + /// This is not general host-loopback ingress support. + pub(crate) fn supports_proxy_loopback_compatibility() -> Result { + if !Self::is_base_container_api_present() { + return Ok(false); + } + Self::psec_1_0_only(secenv::supports_version) + } + + fn psec_1_0_only( + mut supports_version: impl FnMut(SecurityEnvironmentVersion) -> Result, + ) -> Result { + // A 1.1+ host must keep using the advertised ingress support flag, + // even if it also accepts a 1.0 payload. + if supports_version(SecurityEnvironmentVersion::V1_1)? { + return Ok(false); + } + supports_version(SecurityEnvironmentVersion::V1_0) + } + // Temporary compatibility for explicit identity-less proxies on PSEC 1.0-only // hosts. Remove once those hosts support PSEC 1.1; keep loopback allow opt-in. fn psec_1_0_proxy_loopback_workaround( request: &ExecutionRequest, - mut supports_version: impl FnMut( - SecurityEnvironmentVersion, - ) - -> Result, - ) -> Result { + supports_version: impl FnMut(SecurityEnvironmentVersion) -> Result, + ) -> Result { let policy = &request.policy; if !policy.runtime_network_proxy_specified || !policy.network_proxy.is_enabled() @@ -345,12 +362,7 @@ impl BaseContainerRunner { { return Ok(false); } - // A 1.1+ host must keep using the advertised ingress support flag, - // even if it also accepts a 1.0 payload. - if supports_version(SecurityEnvironmentVersion::V1_1)? { - return Ok(false); - } - supports_version(SecurityEnvironmentVersion::V1_0) + Self::psec_1_0_only(supports_version) } /// The single PSEC version-selection point: start at 1.0 and raise it to @@ -411,11 +423,12 @@ impl BaseContainerRunner { } if unrestricted_host_loopback_allowed(&request.policy) && !Self::supports_ingress_host_loopback_allow() + && !matches!( + Self::psec_1_0_proxy_loopback_workaround(request, secenv::supports_version), + Ok(true) + ) { - match Self::psec_1_0_proxy_loopback_workaround(request, secenv::supports_version) { - Ok(true) => {} - Ok(false) | Err(_) => return BaseContainerRequestDecision::IngressUnsupported, - } + return BaseContainerRequestDecision::IngressUnsupported; } BaseContainerRequestDecision::Serviceable } @@ -2939,6 +2952,35 @@ mod tests { request } + #[test] + fn proxy_loopback_compatibility_requires_base_container_api() { + let _guard = crate::process_container_common::test_env::BcUsableGuard::set(false); + assert!(!BaseContainerRunner::supports_proxy_loopback_compatibility().unwrap()); + } + + #[test] + fn proxy_loopback_compatibility_requires_psec_1_0_only() { + for (supports_1_0, supports_1_1, expected) in [ + (true, false, true), + (true, true, false), + (false, true, false), + (false, false, false), + ] { + let supported = BaseContainerRunner::psec_1_0_only(|version| { + Ok(if version == SecurityEnvironmentVersion::V1_1 { + supports_1_1 + } else { + supports_1_0 + }) + }) + .unwrap(); + assert_eq!( + supported, expected, + "PSEC 1.0={supports_1_0}, PSEC 1.1={supports_1_1}" + ); + } + } + #[test] fn psec_1_0_proxy_workaround_requires_a_1_0_only_host() { let request = psec_1_0_proxy_request(); @@ -3009,7 +3051,7 @@ mod tests { let result = BaseContainerRunner::psec_1_0_proxy_loopback_workaround(&request, |version| { if version == failing_version { - Err(learning_mode_windows::LearningModeError::HResultCall { + Err(LearningModeError::HResultCall { function: "IsProcessSecurityEnvironmentVersionSupported", code: E_NOTIMPL.0, }) @@ -3017,10 +3059,7 @@ mod tests { Ok(false) } }); - assert!(matches!( - result, - Err(learning_mode_windows::LearningModeError::HResultCall { .. }) - )); + assert!(matches!(result, Err(LearningModeError::HResultCall { .. }))); } } @@ -3041,7 +3080,7 @@ mod tests { .any(|capability| capability == "networkLoopback")); assert_eq!( network.allowed_appcontainer_peer(), - Some(crate::base_container_helpers::LOOPBACK_NETWORK_PEER) + Some(crate::process_container_common::base_container_helpers::LOOPBACK_NETWORK_PEER) ); assert_eq!( network.proxy().unwrap().url(), diff --git a/src/mxc-sdk/src/backends/process_container/common/probe.rs b/src/mxc-sdk/src/backends/process_container/common/probe.rs index 7cdc519eb..8d73e7bd6 100644 --- a/src/mxc-sdk/src/backends/process_container/common/probe.rs +++ b/src/mxc-sdk/src/backends/process_container/common/probe.rs @@ -74,6 +74,9 @@ pub struct ProbeFacts { /// Whether BaseContainer can honor /// `network.ingress.hostLoopback = "allow"`. pub base_container_supports_ingress_host_loopback_allow: bool, + /// Whether the PSEC 1.0-only identity-less proxy loopback workaround is + /// available. Requires explicit host-loopback allow, not general ingress. + pub base_container_supports_proxy_loopback_compatibility: bool, /// Whether the in-proc IsolationSession service can be activated on this /// host. Always `false` here — `process_container_common` has no dependency on /// the isolation-session backend; `wxc-exec --probe` overrides it when @@ -137,6 +140,7 @@ impl From for UiCapabilitySupport { pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> ProbeOutput { use crate::process_container_common::base_container_runner::BaseContainerRunner; + let proxy_loopback_compatibility = BaseContainerRunner::supports_proxy_loopback_compatibility(); let probes = ProbeFacts { base_container_api_present: BaseContainerRunner::is_base_container_api_present(), native_capture_available: BaseContainerRunner::is_native_capture_available(), @@ -150,17 +154,27 @@ pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> base_container_supports_enumerate_paths: BaseContainerRunner::supports_enumerate_paths(), base_container_supports_ingress_host_loopback_allow: BaseContainerRunner::supports_ingress_host_loopback_allow(), + base_container_supports_proxy_loopback_compatibility: matches!( + proxy_loopback_compatibility, + Ok(true) + ), isolation_session_available: false, hyperlight_available: false, ui_capabilities: crate::process_container_common::job_object::supported_ui_restrictions() .into(), }; - run_probe_with_tier_decision( + let mut output = run_probe_with_tier_decision( request, probes, fallback_detector::choose_backend_tier(request), - ) + ); + if let Err(error) = proxy_loopback_compatibility { + output.warnings.push(format!( + "failed to query PSEC proxy loopback compatibility support: {error}" + )); + } + output } fn run_probe_with_tier_decision( @@ -269,6 +283,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, + base_container_supports_proxy_loopback_compatibility: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -290,6 +305,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, + base_container_supports_proxy_loopback_compatibility: true, isolation_session_available: true, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -312,6 +328,10 @@ mod tests { v["probes"]["baseContainerSupportsIngressHostLoopbackAllow"], false ); + assert_eq!( + v["probes"]["baseContainerSupportsProxyLoopbackCompatibility"], + true + ); assert_eq!(v["probes"]["isolationSessionAvailable"], true); assert_eq!(v["probes"]["uiCapabilities"]["canBlockClipboardRead"], true); assert_eq!( @@ -340,6 +360,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, + base_container_supports_proxy_loopback_compatibility: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: UiCapabilitySupport { diff --git a/src/mxc-sdk/src/core/mxc_engine/probe.rs b/src/mxc-sdk/src/core/mxc_engine/probe.rs index 96c983aa2..64973ef9d 100644 --- a/src/mxc-sdk/src/core/mxc_engine/probe.rs +++ b/src/mxc-sdk/src/core/mxc_engine/probe.rs @@ -30,6 +30,9 @@ pub enum BackendCapability { IngressHostLoopbackAllow, /// Bubblewrap proxy-only egress in a private network namespace. ProxyEnforcement, + /// Identity-less proxy loopback compatibility on PSEC 1.0-only hosts. + /// Requires explicit host-loopback allow; not general ingress support. + ProxyLoopbackCompatibility, } /// One host-available backend, plus its effective isolation tier (if any). @@ -96,18 +99,24 @@ pub fn available_backends() -> Vec { BaseContainerRunner::is_base_container_api_present(), cfg!(feature = "tier2_bfs"), ); - windows_backends( - tier, - ProcessContainerCapabilities { - capture_denials: capture_denials_available( - crate::process_container_common::base_container_runner::BaseContainerRunner::is_native_capture_available(), - guarded_capture::is_available(), - ), - filesystem_denied_paths: crate::process_container_common::base_container_runner::BaseContainerRunner::supports_native_denied_paths(), - filesystem_enumerate_paths: crate::process_container_common::base_container_runner::BaseContainerRunner::supports_enumerate_paths(), - ingress_host_loopback_allow: crate::process_container_common::base_container_runner::BaseContainerRunner::supports_ingress_host_loopback_allow(), - }, - ) + let mut support = ProcessContainerCapabilities { + capture_denials: capture_denials_available( + BaseContainerRunner::is_native_capture_available(), + guarded_capture::is_available(), + ), + filesystem_denied_paths: BaseContainerRunner::supports_native_denied_paths(), + filesystem_enumerate_paths: BaseContainerRunner::supports_enumerate_paths(), + ingress_host_loopback_allow: BaseContainerRunner::supports_ingress_host_loopback_allow( + ), + ..Default::default() + }; + match BaseContainerRunner::supports_proxy_loopback_compatibility() { + Ok(supported) => support.proxy_loopback_compatibility = supported, + Err(error) => support.warnings.push(format!( + "failed to query PSEC proxy loopback compatibility support: {error}" + )), + } + windows_backends(tier, support) } #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] { @@ -165,12 +174,14 @@ fn bubblewrap_backend(proxy_enforcement: Result<(), String>) -> AvailableBackend } #[cfg(target_os = "windows")] -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Default)] struct ProcessContainerCapabilities { capture_denials: bool, filesystem_denied_paths: bool, filesystem_enumerate_paths: bool, ingress_host_loopback_allow: bool, + proxy_loopback_compatibility: bool, + warnings: Vec, } #[cfg(target_os = "windows")] @@ -199,12 +210,15 @@ fn windows_backends( if support.ingress_host_loopback_allow { capabilities.push(BackendCapability::IngressHostLoopbackAllow); } + if support.proxy_loopback_compatibility { + capabilities.push(BackendCapability::ProxyLoopbackCompatibility); + } } let process_container = AvailableBackend { backend: ContainmentBackend::ProcessContainer.wire_name().to_string(), tier: Some(tier.as_str().to_string()), capabilities, - warnings: Vec::new(), + warnings: support.warnings, }; let mut backends = vec![process_container]; @@ -397,6 +411,11 @@ mod tests { .expect("serializes"), r#""ingressHostLoopbackAllow""# ); + assert_eq!( + serde_json::to_string(&BackendCapability::ProxyLoopbackCompatibility) + .expect("serializes"), + r#""proxyLoopbackCompatibility""# + ); } #[test] @@ -531,6 +550,7 @@ mod tests { filesystem_denied_paths: true, filesystem_enumerate_paths: true, ingress_host_loopback_allow: true, + proxy_loopback_compatibility: true, ..Default::default() }, ); @@ -543,6 +563,51 @@ mod tests { } } + #[cfg(target_os = "windows")] + #[test] + fn windows_reports_proxy_compatibility_without_general_ingress_support() { + use crate::process_container_common::fallback_detector::IsolationTier; + + for supported in [false, true] { + let backends = windows_backends( + IsolationTier::BaseContainer, + ProcessContainerCapabilities { + proxy_loopback_compatibility: supported, + ..Default::default() + }, + ); + let process_container = &backends[0]; + assert_eq!( + process_container + .capabilities + .contains(&BackendCapability::ProxyLoopbackCompatibility), + supported + ); + assert!(!process_container + .capabilities + .contains(&BackendCapability::IngressHostLoopbackAllow)); + } + } + + #[cfg(target_os = "windows")] + #[test] + fn windows_preserves_capability_probe_warnings() { + use crate::process_container_common::fallback_detector::IsolationTier; + + let warning = "failed to query PSEC proxy loopback compatibility support".to_string(); + let backends = windows_backends( + IsolationTier::BaseContainer, + ProcessContainerCapabilities { + warnings: vec![warning.clone()], + ..Default::default() + }, + ); + assert_eq!(backends[0].warnings, vec![warning]); + assert!(!backends[0] + .capabilities + .contains(&BackendCapability::ProxyLoopbackCompatibility)); + } + #[cfg(target_os = "windows")] #[test] fn tier_precedence_prefers_the_strongest_reachable_rung() { From 929c4aed0ecdd173e147ee9aee3118a01d3cb948 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 12:32:50 -0300 Subject: [PATCH 03/10] fix(tests): require proxy compatibility from host capability Gate the PSEC 1.0 workaround assertions on the host probe instead of the runtime log. Require launch and the compatibility log, and exercise proxy reachability even when the first workload fails on an eligible host. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671f3dd1-048a-4b89-acb6-e762eae992af Signed-off-by: Carlos Alexandro Becker --- tests/scripts/README.md | 4 ++++ tests/scripts/lib/WinProcessContainer.Common.ps1 | 1 + .../run_processcontainer_network_proxy_test.ps1 | 11 +++++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/scripts/README.md b/tests/scripts/README.md index 2507c96a6..34734598d 100644 --- a/tests/scripts/README.md +++ b/tests/scripts/README.md @@ -58,6 +58,10 @@ built tree, which is the fastest way to iterate on one area: tests\scripts\run_processcontainer_network_proxy_test.ps1 -RequireTier base-container ``` +When the host probe reports `baseContainerSupportsProxyLoopbackCompatibility`, +the proxy area requires workload launch, the compatibility log, and a successful +proxied fetch. A clean policy rejection is a failure on these hosts. + Shared helpers live in `tests/scripts/lib/WinProcessContainer.Common.ps1`. It must be **dot-sourced, not imported as a module** — `Initialize-WpcContext` publishes the suite context into the calling script's scope, which only works diff --git a/tests/scripts/lib/WinProcessContainer.Common.ps1 b/tests/scripts/lib/WinProcessContainer.Common.ps1 index 497eacca2..18dc1d464 100644 --- a/tests/scripts/lib/WinProcessContainer.Common.ps1 +++ b/tests/scripts/lib/WinProcessContainer.Common.ps1 @@ -387,6 +387,7 @@ function Get-HostCapabilities { # denied tests auto-enable when it ships. SupportsDeniedPaths = (($tier -eq 'appcontainer-dacl') -or $denyBit) BaseContainerSupportsEnumeratePaths = $enumBit + BaseContainerSupportsProxyLoopbackCompatibility = [bool]$p.probes.baseContainerSupportsProxyLoopbackCompatibility # enumeratePaths has NO fallback: it needs PSEC 1.1 plus # PSE_SUPPORT_FS_ENUMERATE, and the detector refuses the request # outright on every AppContainer tier (FallbackError:: diff --git a/tests/scripts/run_processcontainer_network_proxy_test.ps1 b/tests/scripts/run_processcontainer_network_proxy_test.ps1 index d5695a8f1..fc4cf6fa6 100644 --- a/tests/scripts/run_processcontainer_network_proxy_test.ps1 +++ b/tests/scripts/run_processcontainer_network_proxy_test.ps1 @@ -87,6 +87,7 @@ function Invoke-NetworkProxyAssertions { if ($psec) { $out = $envRun.Result.Stdout $ran = [bool]($out -match '(?im)^SystemRoot=') + $compatibilityAvailable = $Script:Caps.BaseContainerSupportsProxyLoopbackCompatibility # PSEC 1.0-only hosts use the explicit identity-less proxy workaround. # PSEC 1.1+ still requires the ingress support flag. A bare OS error # is never a valid substitute for successful execution or rejection. @@ -95,11 +96,13 @@ function Invoke-NetworkProxyAssertions { -Pass ($ran -or $rejectedCleanly) ` -Detail ("ran=$ran; rejectedAtValidation=$rejectedCleanly; exit=$($envRun.Result.ExitCode); " + "stderr=$(Format-Snippet $envRun.Result.Stderr)") - if ($envRun.Log -match 'using PSEC 1\.0 proxy loopback compatibility') { - Record-Result -Phase 'P8e' -Name 'PSEC 1.0 proxy workaround launches the workload' ` - -Pass $ran -Detail "exit=$($envRun.Result.ExitCode); ran=$ran" + if ($compatibilityAvailable) { + $compatibilityLogged = [bool]($envRun.Log -match 'using PSEC 1\.0 proxy loopback compatibility') + Record-Result -Phase 'P8e' -Name 'PSEC 1.0-capable host launches and logs the proxy workaround' ` + -Pass ($ran -and $compatibilityLogged) ` + -Detail "exit=$($envRun.Result.ExitCode); ran=$ran; compatibilityLogged=$compatibilityLogged" } - if ($ran) { + if ($ran -or $compatibilityAvailable) { foreach ($v in @('HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy')) { # cmd.exe `set` upper-cases nothing, but Windows env lookup is # case-insensitive and duplicate-insensitive, so a variable set From a7b85eca5ee365b6427ab75e303318cf90d83672 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 12:32:50 -0300 Subject: [PATCH 04/10] docs(sdk): fix proxy compatibility reference link Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671f3dd1-048a-4b89-acb6-e762eae992af Signed-off-by: Carlos Alexandro Becker --- src/mxc-sdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mxc-sdk/README.md b/src/mxc-sdk/README.md index 5852eb89d..0d683689d 100644 --- a/src/mxc-sdk/README.md +++ b/src/mxc-sdk/README.md @@ -145,7 +145,7 @@ For the PSEC 1.0-only identity-less proxy workaround, check `v1::BackendCapability::ProxyLoopbackCompatibility` or, on Windows, `v1::probe(None)?.probes.base_container_supports_proxy_loopback_compatibility`. This is not general host-loopback ingress support; see -[proxy compatibility discovery](../../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). +[proxy compatibility discovery](../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). Telemetry and policy helpers are also under `v1`. The [launch-choice table](../../../docs/reference/rust/v1/api.md#choosing-a-launch-operation) compares captured, piped, and terminal execution. From 3c030b366dc524de2f2c46b08f63f54d675549c0 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 13:07:58 -0300 Subject: [PATCH 05/10] fix(tests): tolerate older proxy capability probes Preserve the harness's optional probe-field behavior under StrictMode. Missing proxy compatibility facts remain false; explicit true and false remain unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671f3dd1-048a-4b89-acb6-e762eae992af Signed-off-by: Carlos Alexandro Becker --- tests/scripts/README.md | 2 ++ tests/scripts/lib/WinProcessContainer.Common.ps1 | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/scripts/README.md b/tests/scripts/README.md index 34734598d..c8bcbf965 100644 --- a/tests/scripts/README.md +++ b/tests/scripts/README.md @@ -61,6 +61,8 @@ tests\scripts\run_processcontainer_network_proxy_test.ps1 -RequireTier base-cont When the host probe reports `baseContainerSupportsProxyLoopbackCompatibility`, the proxy area requires workload launch, the compatibility log, and a successful proxied fetch. A clean policy rejection is a failure on these hosts. +Older binaries that omit this probe fact do not enable the compatibility-specific +assertions; the existing proxy assertions still apply. Shared helpers live in `tests/scripts/lib/WinProcessContainer.Common.ps1`. It must be **dot-sourced, not imported as a module** — `Initialize-WpcContext` diff --git a/tests/scripts/lib/WinProcessContainer.Common.ps1 b/tests/scripts/lib/WinProcessContainer.Common.ps1 index 18dc1d464..b8e77ceb5 100644 --- a/tests/scripts/lib/WinProcessContainer.Common.ps1 +++ b/tests/scripts/lib/WinProcessContainer.Common.ps1 @@ -351,6 +351,9 @@ function Get-HostCapabilities { $enumBit = if ($p.probes.PSObject.Properties['baseContainerSupportsEnumeratePaths']) { [bool]$p.probes.baseContainerSupportsEnumeratePaths } else { $false } + $proxyCompatibilityBit = if ($p.probes.PSObject.Properties['baseContainerSupportsProxyLoopbackCompatibility']) { + [bool]$p.probes.baseContainerSupportsProxyLoopbackCompatibility + } else { $false } # uiCapabilities is absent on older binaries / when the detector errored. $canInject = $false if ($p.probes.PSObject.Properties['uiCapabilities'] -and @@ -387,7 +390,7 @@ function Get-HostCapabilities { # denied tests auto-enable when it ships. SupportsDeniedPaths = (($tier -eq 'appcontainer-dacl') -or $denyBit) BaseContainerSupportsEnumeratePaths = $enumBit - BaseContainerSupportsProxyLoopbackCompatibility = [bool]$p.probes.baseContainerSupportsProxyLoopbackCompatibility + BaseContainerSupportsProxyLoopbackCompatibility = $proxyCompatibilityBit # enumeratePaths has NO fallback: it needs PSEC 1.1 plus # PSE_SUPPORT_FS_ENUMERATE, and the detector refuses the request # outright on every AppContainer tier (FallbackError:: From f4d81337e916f768120662eaa89e8e066583a79c Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 18:34:00 -0300 Subject: [PATCH 06/10] docs(sdk): remove duplicated proxy compatibility notes Address review feedback by keeping the compatibility explanation in the networking guide rather than repeating it in each V1 type reference. Leave all API declarations unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 671f3dd1-048a-4b89-acb6-e762eae992af Signed-off-by: Carlos Alexandro Becker --- docs/reference/dotnet/v1/types.md | 4 ---- docs/reference/node/v1/types.md | 4 ---- docs/reference/rust/v1/types.md | 4 ---- 3 files changed, 12 deletions(-) diff --git a/docs/reference/dotnet/v1/types.md b/docs/reference/dotnet/v1/types.md index a667671ae..e85bfe391 100644 --- a/docs/reference/dotnet/v1/types.md +++ b/docs/reference/dotnet/v1/types.md @@ -40,10 +40,6 @@ public enum BackendCapability } ``` -`ProxyLoopbackCompatibility` reports the identity-less proxy workaround on -PSEC 1.0-only hosts, not general ingress support. See -[proxy compatibility discovery](../../../process-container/networking.md#discovering-proxy-loopback-compatibility). - ## `Microsoft.Mxc.Sdk.V1.CaptureDenialsError` diff --git a/docs/reference/node/v1/types.md b/docs/reference/node/v1/types.md index 4ea474033..d5c8222d8 100644 --- a/docs/reference/node/v1/types.md +++ b/docs/reference/node/v1/types.md @@ -33,10 +33,6 @@ export type BackendCapability = | 'unknown'; ``` -`proxyLoopbackCompatibility` reports the identity-less proxy workaround on -PSEC 1.0-only hosts, not general ingress support. See -[proxy compatibility discovery](../../../process-container/networking.md#discovering-proxy-loopback-compatibility). - ## `@microsoft/mxc-sdk/v1::BaseProcessUiConfig` BaseProcess-specific UI configuration (Windows only). diff --git a/docs/reference/rust/v1/types.md b/docs/reference/rust/v1/types.md index cc2aea43c..f040c8245 100644 --- a/docs/reference/rust/v1/types.md +++ b/docs/reference/rust/v1/types.md @@ -33,10 +33,6 @@ pub enum BackendCapability { } ``` -`ProxyLoopbackCompatibility` reports the identity-less proxy workaround on -PSEC 1.0-only hosts, not general ingress support. See -[proxy compatibility discovery](../../../process-container/networking.md#discovering-proxy-loopback-compatibility). - ## `mxc_sdk::v1::BubblewrapNetworkSupport` From a0defb90b3b47a2c1a1495073abe1d296c3a4579 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 19:13:40 -0300 Subject: [PATCH 07/10] fix(probe): report identityless loopback proxy support Address review feedback on capability naming and PSEC 1.1 support across all SDKs. Remove the SDK README details and redundant runtime warning, preserving capability-driven launch and proxy-reachability assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cfef35-3eab-4f33-ba4c-3408938fc50d Signed-off-by: Carlos Alexandro Becker --- docs/process-container/networking.md | 18 ++-- docs/reference/dotnet/v1/types.md | 4 +- docs/reference/node/v1/types.md | 4 +- docs/reference/rust/v1/types.md | 4 +- .../Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs | 12 +-- .../V1/MxcContainerTests.cs | 29 +++--- .../Microsoft.Mxc.Sdk/V1/MxcContainer.cs | 4 +- .../Microsoft.Mxc.Sdk/V1/MxcPlatform.cs | 2 +- .../Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs | 12 +-- sdk/dotnet/README.md | 6 -- sdk/node/README.md | 6 -- sdk/node/src/v1/platform.ts | 2 +- sdk/node/src/v1/probe.ts | 2 +- sdk/node/src/v1/types.ts | 6 +- sdk/node/tests/unit/platform.test.ts | 6 +- sdk/node/tests/unit/probe.test.ts | 17 ++-- src/ffi/mxc_ffi/tests/ffi.rs | 2 +- src/mxc-sdk/README.md | 5 - .../common/base_container_runner.rs | 94 ++++++++++++++----- .../process_container/common/probe.rs | 24 ++--- src/mxc-sdk/src/core/mxc_engine/probe.rs | 44 +++++---- tests/scripts/README.md | 9 +- .../lib/WinProcessContainer.Common.ps1 | 6 +- ...un_processcontainer_network_proxy_test.ps1 | 12 +-- 24 files changed, 182 insertions(+), 148 deletions(-) diff --git a/docs/process-container/networking.md b/docs/process-container/networking.md index c0061a3ba..6a443e8cf 100644 --- a/docs/process-container/networking.md +++ b/docs/process-container/networking.md @@ -125,17 +125,21 @@ flag; accepting a 1.0 payload does not enable this workaround. Requests that do not allow host loopback use the PSEC 1.0 capability mapping. Requests the PSEC contract cannot preserve continue to an AppContainer tier. -#### Discovering proxy loopback compatibility +#### Discovering identity-less loopback proxy support -Backend discovery reports `proxyLoopbackCompatibility` on the ProcessContainer -`base-container` tier only when the PSEC 1.0-only workaround is available. +Backend discovery reports `identitylessLoopbackProxy` on the ProcessContainer +`base-container` tier when the host supports an identity-less proxy on loopback. The Windows request probe also reports -`probes.baseContainerSupportsProxyLoopbackCompatibility`, including when called -without a request. Both use the same PSEC version gate as execution. A query -failure omits the capability, sets the probe fact to `false`, and adds a warning. +`probes.baseContainerSupportsIdentitylessLoopbackProxy`, including when called +without a request. Support is available on PSEC 1.0-only hosts and on hosts with +PSEC 1.1 or newer that advertise ingress support. A host that reports +`baseContainerSupportsIngressHostLoopbackAllow` also supports an identity-less +loopback proxy. PSEC 1.1 or newer without ingress support does not qualify, +even if it also accepts PSEC 1.0 payloads. Version-query failures omit the +capability, set the probe fact to `false`, and add a warning. This does not grant general `ingressHostLoopbackAllow` support or report that the -current request uses the workaround. The request must set +current request uses an identity-less proxy. The request must set `runtimeConfig.networkProxy`, omit `processContainer.network.allowedProxyPeer`, and explicitly allow host loopback, while retaining the Model 2 network policy. Other requested features still require their own host support. diff --git a/docs/reference/dotnet/v1/types.md b/docs/reference/dotnet/v1/types.md index 29ca5eb38..28ab98ff2 100644 --- a/docs/reference/dotnet/v1/types.md +++ b/docs/reference/dotnet/v1/types.md @@ -38,7 +38,7 @@ public enum BackendCapability FilesystemDeniedPaths = 3, IngressHostLoopbackAllow = 4, FilesystemEnumeratePaths = 5, - ProxyLoopbackCompatibility = 6, + IdentitylessLoopbackProxy = 6, } ``` @@ -955,7 +955,7 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } - public bool BaseContainerSupportsProxyLoopbackCompatibility { get; init; } + public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } diff --git a/docs/reference/node/v1/types.md b/docs/reference/node/v1/types.md index f3b8262c3..83b11b3eb 100644 --- a/docs/reference/node/v1/types.md +++ b/docs/reference/node/v1/types.md @@ -31,7 +31,7 @@ export type BackendCapability = | 'filesystemEnumeratePaths' | 'ingressHostLoopbackAllow' | 'proxyEnforcement' - | 'proxyLoopbackCompatibility' + | 'identitylessLoopbackProxy' | 'unknown'; ``` @@ -579,7 +579,7 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; - baseContainerSupportsProxyLoopbackCompatibility: boolean; + baseContainerSupportsIdentitylessLoopbackProxy: boolean; isolationSessionAvailable: boolean; hyperlightAvailable: boolean; uiCapabilities: UiCapabilitySupport; diff --git a/docs/reference/rust/v1/types.md b/docs/reference/rust/v1/types.md index e4d4bd61f..56dafe324 100644 --- a/docs/reference/rust/v1/types.md +++ b/docs/reference/rust/v1/types.md @@ -31,7 +31,7 @@ pub enum BackendCapability { FilesystemEnumeratePaths, IngressHostLoopbackAllow, ProxyEnforcement, - ProxyLoopbackCompatibility, + IdentitylessLoopbackProxy, } ``` @@ -536,7 +536,7 @@ pub struct ProbeFacts { pub base_container_supports_deny_paths: bool, pub base_container_supports_enumerate_paths: bool, pub base_container_supports_ingress_host_loopback_allow: bool, - pub base_container_supports_proxy_loopback_compatibility: bool, + pub base_container_supports_identityless_loopback_proxy: bool, pub isolation_session_available: bool, pub hyperlight_available: bool, pub ui_capabilities: UiCapabilitySupport, diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs index 5b239221c..4c8ffeb2a 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs @@ -338,7 +338,7 @@ static void CheckRoundTrip(T value, Func check, string name) "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, "baseContainerSupportsIngressHostLoopbackAllow": false, - "baseContainerSupportsProxyLoopbackCompatibility": true, + "baseContainerSupportsIdentitylessLoopbackProxy": true, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -360,14 +360,14 @@ static void CheckRoundTrip(T value, Func check, string name) Check(probe.Tier == IsolationTier.AppContainerDacl, "probe tier parsed"); Check(probe.NeedsDaclAugmentation == true, "probe dacl augmentation parsed"); Check(probe.Probes.BaseContainerApiPresent, "probe facts parsed"); -Check(probe.Probes.BaseContainerSupportsProxyLoopbackCompatibility, "proxy loopback compatibility parsed"); +Check(probe.Probes.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy parsed"); Check(probe.Probes.UiCapabilities.CanBlockClipboardRead, "probe ui capabilities parsed"); var nativeProbe = MxcJson.Deserialize(probeJson, MxcJson.ProbeOptions)!; CheckRoundTrip(nativeProbe.Probes!, - x => x.BaseContainerSupportsProxyLoopbackCompatibility, "proxy loopback compatibility facts"); + x => x.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy facts"); var discovery = MxcPlatform.ParseAvailableBackends( - """[{"backend":"processcontainer","tier":"base-container","capabilities":["proxyLoopbackCompatibility"]}]"""); -Check(discovery.Single().Capabilities.SequenceEqual([BackendCapability.ProxyLoopbackCompatibility]), - "proxy loopback compatibility discovery"); + """[{"backend":"processcontainer","tier":"base-container","capabilities":["identitylessLoopbackProxy"]}]"""); +Check(discovery.Single().Capabilities.SequenceEqual([BackendCapability.IdentitylessLoopbackProxy]), + "identity-less loopback proxy discovery"); Console.WriteLine("AOT smoke test passed: all JSON paths are reflection-free."); diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs index 45ac4e408..19587b073 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs @@ -30,7 +30,7 @@ public class MxcContainerTests "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, "baseContainerSupportsIngressHostLoopbackAllow": true, - "baseContainerSupportsProxyLoopbackCompatibility": false, + "baseContainerSupportsIdentitylessLoopbackProxy": true, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -274,37 +274,38 @@ public void ProbeParser_RejectsMissingRequiredProbeBoolean() } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ProbeParser_PreservesProxyLoopbackCompatibility(bool supported) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void ProbeParser_PreservesIdentitylessLoopbackProxy(bool supported, bool ingressSupported) { var json = CreateCompleteProbeJson(); json["tier"] = "base-container"; json["needsDaclAugmentation"] = false; - json["probes"]!["baseContainerSupportsIngressHostLoopbackAllow"] = false; - json["probes"]!["baseContainerSupportsProxyLoopbackCompatibility"] = supported; + json["probes"]!["baseContainerSupportsIngressHostLoopbackAllow"] = ingressSupported; + json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = supported; var output = MxcContainer.ParseProbeOutput(json.ToJsonString()); - Assert.Equal(supported, output.Probes.BaseContainerSupportsProxyLoopbackCompatibility); - Assert.False(output.Probes.BaseContainerSupportsIngressHostLoopbackAllow); + Assert.Equal(supported, output.Probes.BaseContainerSupportsIdentitylessLoopbackProxy); + Assert.Equal(ingressSupported, output.Probes.BaseContainerSupportsIngressHostLoopbackAllow); } [Fact] - public void ProbeParser_RejectsMissingProxyLoopbackCompatibility() + public void ProbeParser_RejectsMissingIdentitylessLoopbackProxy() { AssertProbeJsonRejected(json => - json["probes"]!.AsObject().Remove("baseContainerSupportsProxyLoopbackCompatibility")); + json["probes"]!.AsObject().Remove("baseContainerSupportsIdentitylessLoopbackProxy")); } [Theory] [InlineData("null")] [InlineData("\"true\"")] [InlineData("1")] - public void ProbeParser_RejectsInvalidProxyLoopbackCompatibility(string value) + public void ProbeParser_RejectsInvalidIdentitylessLoopbackProxy(string value) { AssertProbeJsonRejected(json => - json["probes"]!["baseContainerSupportsProxyLoopbackCompatibility"] = JsonNode.Parse(value)); + json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = JsonNode.Parse(value)); } [Fact] @@ -491,7 +492,7 @@ public void Discovery_MapsEveryNativeIsolationTier( [InlineData("filesystemEnumeratePaths", BackendCapability.FilesystemEnumeratePaths)] [InlineData("ingressHostLoopbackAllow", BackendCapability.IngressHostLoopbackAllow)] [InlineData("proxyEnforcement", BackendCapability.ProxyEnforcement)] - [InlineData("proxyLoopbackCompatibility", BackendCapability.ProxyLoopbackCompatibility)] + [InlineData("identitylessLoopbackProxy", BackendCapability.IdentitylessLoopbackProxy)] public void Discovery_MapsEveryNativeCapability( string wireName, BackendCapability expected) @@ -508,7 +509,7 @@ public void BackendCapability_PreservesReleasedNumericValues() Assert.Equal(3, (int)BackendCapability.FilesystemDeniedPaths); Assert.Equal(4, (int)BackendCapability.IngressHostLoopbackAllow); Assert.Equal(5, (int)BackendCapability.FilesystemEnumeratePaths); - Assert.Equal(6, (int)BackendCapability.ProxyLoopbackCompatibility); + Assert.Equal(6, (int)BackendCapability.IdentitylessLoopbackProxy); } [Theory] diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs index c1e6a06a0..611da275f 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs @@ -177,8 +177,8 @@ internal static ProbeOutput ParseProbeOutput(string json) probes.BaseContainerSupportsEnumeratePaths, BaseContainerSupportsIngressHostLoopbackAllow = probes.BaseContainerSupportsIngressHostLoopbackAllow, - BaseContainerSupportsProxyLoopbackCompatibility = - probes.BaseContainerSupportsProxyLoopbackCompatibility, + BaseContainerSupportsIdentitylessLoopbackProxy = + probes.BaseContainerSupportsIdentitylessLoopbackProxy, IsolationSessionAvailable = probes.IsolationSessionAvailable, HyperlightAvailable = probes.HyperlightAvailable, UiCapabilities = new UiCapabilitySupport diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs index 2a6df8549..53db9685d 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcPlatform.cs @@ -118,7 +118,7 @@ internal static BackendCapability ParseBackendCapability(string value) => "filesystemEnumeratePaths" => BackendCapability.FilesystemEnumeratePaths, "ingressHostLoopbackAllow" => BackendCapability.IngressHostLoopbackAllow, "proxyEnforcement" => BackendCapability.ProxyEnforcement, - "proxyLoopbackCompatibility" => BackendCapability.ProxyLoopbackCompatibility, + "identitylessLoopbackProxy" => BackendCapability.IdentitylessLoopbackProxy, _ => BackendCapability.Unknown, }; diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs index 7f38bc310..b7eed9cfc 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs @@ -80,10 +80,10 @@ public enum BackendCapability FilesystemEnumeratePaths = 5, /// - /// Identity-less proxy loopback workaround on PSEC 1.0-only hosts. + /// Identity-less proxy support on loopback. /// Requires explicit host-loopback allow; not general ingress support. /// - ProxyLoopbackCompatibility = 6, + IdentitylessLoopbackProxy = 6, } /// One host-available backend and its probed capabilities. @@ -152,8 +152,8 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } - /// PSEC 1.0-only identity-less proxy workaround, not general ingress support. - public bool BaseContainerSupportsProxyLoopbackCompatibility { get; init; } + /// Identity-less proxy support on loopback; requires explicit host-loopback allow. + public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } @@ -288,8 +288,8 @@ internal sealed class NativeProbeFacts public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } [JsonRequired] - [JsonPropertyName("baseContainerSupportsProxyLoopbackCompatibility")] - public bool BaseContainerSupportsProxyLoopbackCompatibility { get; init; } + [JsonPropertyName("baseContainerSupportsIdentitylessLoopbackProxy")] + public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } [JsonRequired] [JsonPropertyName("isolationSessionAvailable")] diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md index 21fff1c6b..ac44389a0 100644 --- a/sdk/dotnet/README.md +++ b/sdk/dotnet/README.md @@ -237,12 +237,6 @@ sandbox on the current host. `MxcPlatform.GetAvailableBackends()` reports host backend capabilities; availability is advisory and launch-time validation still applies. -For the PSEC 1.0-only identity-less proxy workaround, check -`BackendCapability.ProxyLoopbackCompatibility` or, on Windows, -`MxcContainer.Probe().Probes.BaseContainerSupportsProxyLoopbackCompatibility`. -This is not general host-loopback ingress support; see -[proxy compatibility discovery](../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). - Creation telemetry is supplied through `Telemetry` on `RunOptions`, `SpawnOptions`, or `SpawnWithPtyOptions`, not on `ContainerRequest`. Omission leaves telemetry disabled; `new TelemetryConfig { Enabled = false }` diff --git a/sdk/node/README.md b/sdk/node/README.md index fac3974e1..677c20c7e 100644 --- a/sdk/node/README.md +++ b/sdk/node/README.md @@ -256,12 +256,6 @@ through V1 creation. Discovery is advisory, and launch-time validation still applies. Native failures and malformed discovery results throw rather than reporting an unsupported host. -For the PSEC 1.0-only identity-less proxy workaround, check the ProcessContainer -capability `proxyLoopbackCompatibility` or, on Windows, -`probe().probes.baseContainerSupportsProxyLoopbackCompatibility`. -This is not general host-loopback ingress support; see -[proxy compatibility discovery](../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). - Filesystem discovery helpers and their result/options types are grouped under `policy.filesystem` from `@microsoft/mxc-sdk/v1`. They take an optional `environment` map; omission uses diff --git a/sdk/node/src/v1/platform.ts b/sdk/node/src/v1/platform.ts index 610c9e2b0..b728b6024 100644 --- a/sdk/node/src/v1/platform.ts +++ b/sdk/node/src/v1/platform.ts @@ -26,7 +26,7 @@ const discoveryBackends: readonly ContainmentBackend[] = [ const discoveryCapabilities: readonly BackendCapability[] = [ 'captureDenials', 'filesystemDeniedPaths', 'filesystemEnumeratePaths', 'ingressHostLoopbackAllow', 'proxyEnforcement', - 'proxyLoopbackCompatibility', + 'identitylessLoopbackProxy', ]; const discoveryTiers: readonly IsolationTier[] = [ 'base-container', 'appcontainer-bfs', 'appcontainer-dacl', diff --git a/sdk/node/src/v1/probe.ts b/sdk/node/src/v1/probe.ts index a6b7a296d..e16a603e2 100644 --- a/sdk/node/src/v1/probe.ts +++ b/sdk/node/src/v1/probe.ts @@ -31,7 +31,7 @@ const probeFactBooleanFields: readonly (keyof ProbeFacts)[] = [ 'baseContainerSupportsDenyPaths', 'baseContainerSupportsEnumeratePaths', 'baseContainerSupportsIngressHostLoopbackAllow', - 'baseContainerSupportsProxyLoopbackCompatibility', + 'baseContainerSupportsIdentitylessLoopbackProxy', 'isolationSessionAvailable', 'hyperlightAvailable', ]; diff --git a/sdk/node/src/v1/types.ts b/sdk/node/src/v1/types.ts index 6bde66bf3..1aa34404e 100644 --- a/sdk/node/src/v1/types.ts +++ b/sdk/node/src/v1/types.ts @@ -711,7 +711,7 @@ export type BackendCapability = | 'filesystemEnumeratePaths' | 'ingressHostLoopbackAllow' | 'proxyEnforcement' - | 'proxyLoopbackCompatibility' + | 'identitylessLoopbackProxy' | 'unknown'; /** One host-available backend and its native capabilities. */ @@ -785,8 +785,8 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; - /** PSEC 1.0-only identity-less proxy workaround, not general ingress support. */ - baseContainerSupportsProxyLoopbackCompatibility: boolean; + /** Identity-less proxy support on loopback; requires explicit host-loopback allow. */ + baseContainerSupportsIdentitylessLoopbackProxy: boolean; /** True when this executor includes IsolationSession and the host can activate it. */ isolationSessionAvailable: boolean; /** True when this executor includes Hyperlight and its host runtime is available. */ diff --git a/sdk/node/tests/unit/platform.test.ts b/sdk/node/tests/unit/platform.test.ts index cb98a53f8..b3375fa53 100644 --- a/sdk/node/tests/unit/platform.test.ts +++ b/sdk/node/tests/unit/platform.test.ts @@ -71,15 +71,15 @@ describe('native backend discovery', () => { }]); }); - it('reports proxy loopback compatibility without general ingress support', () => { + it('reports identity-less loopback proxy support without general ingress support', () => { assert.deepStrictEqual(parseAvailableBackends(JSON.stringify([{ backend: 'processcontainer', tier: 'base-container', - capabilities: ['proxyLoopbackCompatibility'], + capabilities: ['identitylessLoopbackProxy'], }])), [{ backend: 'processcontainer', tier: 'base-container', - capabilities: ['proxyLoopbackCompatibility'], + capabilities: ['identitylessLoopbackProxy'], warnings: [], }]); }); diff --git a/sdk/node/tests/unit/probe.test.ts b/sdk/node/tests/unit/probe.test.ts index 61d45f761..ddddd93ee 100644 --- a/sdk/node/tests/unit/probe.test.ts +++ b/sdk/node/tests/unit/probe.test.ts @@ -30,7 +30,7 @@ const completeProbe = { baseContainerSupportsDenyPaths: false, baseContainerSupportsEnumeratePaths: false, baseContainerSupportsIngressHostLoopbackAllow: false, - baseContainerSupportsProxyLoopbackCompatibility: false, + baseContainerSupportsIdentitylessLoopbackProxy: false, isolationSessionAvailable: false, hyperlightAvailable: false, uiCapabilities: { @@ -193,29 +193,30 @@ describe('probe', () => { assert.equal(output.needsDaclAugmentation, true); }); - it('preserves proxy loopback compatibility independently of general ingress', () => { - for (const supported of [false, true]) { + it('preserves identity-less proxy support with or without general ingress', () => { + for (const [supported, ingressSupported] of [[false, false], [true, false], [true, true]]) { const output = runProbeOutput({ ...completeProbe, tier: 'base-container', needsDaclAugmentation: false, probes: { ...completeProbe.probes, - baseContainerSupportsProxyLoopbackCompatibility: supported, + baseContainerSupportsIdentitylessLoopbackProxy: supported, + baseContainerSupportsIngressHostLoopbackAllow: ingressSupported, }, }); - assert.equal(output.probes.baseContainerSupportsProxyLoopbackCompatibility, supported); - assert.equal(output.probes.baseContainerSupportsIngressHostLoopbackAllow, false); + assert.equal(output.probes.baseContainerSupportsIdentitylessLoopbackProxy, supported); + assert.equal(output.probes.baseContainerSupportsIngressHostLoopbackAllow, ingressSupported); } }); - it('rejects missing or invalid proxy loopback compatibility facts', () => { + it('rejects missing or invalid identity-less loopback proxy facts', () => { for (const value of [undefined, null, 'true', 1]) { assertProbeOutputRejected({ ...completeProbe, probes: { ...completeProbe.probes, - baseContainerSupportsProxyLoopbackCompatibility: value, + baseContainerSupportsIdentitylessLoopbackProxy: value, }, }); } diff --git a/src/ffi/mxc_ffi/tests/ffi.rs b/src/ffi/mxc_ffi/tests/ffi.rs index 905e76845..415bd3bc4 100644 --- a/src/ffi/mxc_ffi/tests/ffi.rs +++ b/src/ffi/mxc_ffi/tests/ffi.rs @@ -78,7 +78,7 @@ fn extern_discovery_returns_owned_json() { | "filesystemEnumeratePaths" | "ingressHostLoopbackAllow" | "proxyEnforcement" - | "proxyLoopbackCompatibility" + | "identitylessLoopbackProxy" ) )); } diff --git a/src/mxc-sdk/README.md b/src/mxc-sdk/README.md index 7cc6321ea..d61c5c40f 100644 --- a/src/mxc-sdk/README.md +++ b/src/mxc-sdk/README.md @@ -143,11 +143,6 @@ the terminal open. Ordinary `spawn` continues to use separate pipes. For host discovery, use `mxc_sdk::v1::platform_support` and `mxc_sdk::v1::available_backends`. Errors are returned as `mxc_sdk::v1::Error` with an `ErrorCode`. -For the PSEC 1.0-only identity-less proxy workaround, check -`v1::BackendCapability::ProxyLoopbackCompatibility` or, on Windows, -`v1::probe(None)?.probes.base_container_supports_proxy_loopback_compatibility`. -This is not general host-loopback ingress support; see -[proxy compatibility discovery](../../docs/process-container/networking.md#discovering-proxy-loopback-compatibility). Telemetry and policy helpers are also under `v1`. The [launch-choice table](../../../docs/reference/rust/v1/api.md#choosing-a-launch-operation) compares captured, piped, and terminal execution. diff --git a/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs b/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs index 04d24ca05..56e565657 100644 --- a/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs +++ b/src/mxc-sdk/src/backends/process_container/common/base_container_runner.rs @@ -328,13 +328,26 @@ impl BaseContainerRunner { && secenv::query_support(SecurityEnvironmentSupport::NetworkIngress) } - /// Whether this host supports the identity-less proxy loopback workaround. + /// Whether this host supports an identity-less proxy on loopback. /// This is not general host-loopback ingress support. - pub(crate) fn supports_proxy_loopback_compatibility() -> Result { + pub(crate) fn supports_identityless_loopback_proxy() -> Result { if !Self::is_base_container_api_present() { return Ok(false); } - Self::psec_1_0_only(secenv::supports_version) + Self::identityless_loopback_proxy_supported( + Self::supports_ingress_host_loopback_allow(), + secenv::supports_version, + ) + } + + fn identityless_loopback_proxy_supported( + ingress_supported: bool, + supports_version: impl FnMut(SecurityEnvironmentVersion) -> Result, + ) -> Result { + if ingress_supported { + return Ok(true); + } + Self::psec_1_0_only(supports_version) } fn psec_1_0_only( @@ -468,13 +481,6 @@ impl BaseContainerRunner { )) }, )?; - if proxy_loopback_workaround { - let _ = writeln!( - logger, - "warning: using PSEC 1.0 proxy loopback compatibility; the proxy is \ - identified only by its endpoint, not by its host process identity" - ); - } let psec_version = Self::choose_min_required_psec_version_for_request(request, proxy_loopback_workaround); let version_supported = @@ -2953,34 +2959,70 @@ mod tests { } #[test] - fn proxy_loopback_compatibility_requires_base_container_api() { + fn identityless_loopback_proxy_requires_base_container_api() { let _guard = crate::process_container_common::test_env::BcUsableGuard::set(false); - assert!(!BaseContainerRunner::supports_proxy_loopback_compatibility().unwrap()); + assert!(!BaseContainerRunner::supports_identityless_loopback_proxy().unwrap()); } #[test] - fn proxy_loopback_compatibility_requires_psec_1_0_only() { - for (supports_1_0, supports_1_1, expected) in [ - (true, false, true), - (true, true, false), - (false, true, false), - (false, false, false), + fn identityless_loopback_proxy_supports_ingress_or_psec_1_0_only() { + for (supports_1_0, supports_1_1, ingress_supported, expected) in [ + (true, false, false, true), + (true, true, true, true), + (true, true, false, false), + (false, true, true, true), + (false, true, false, false), + (false, false, false, false), ] { - let supported = BaseContainerRunner::psec_1_0_only(|version| { - Ok(if version == SecurityEnvironmentVersion::V1_1 { - supports_1_1 - } else { - supports_1_0 - }) - }) + let supported = BaseContainerRunner::identityless_loopback_proxy_supported( + ingress_supported, + |version| { + Ok(if version == SecurityEnvironmentVersion::V1_1 { + supports_1_1 + } else { + supports_1_0 + }) + }, + ) .unwrap(); assert_eq!( supported, expected, - "PSEC 1.0={supports_1_0}, PSEC 1.1={supports_1_1}" + "PSEC 1.0={supports_1_0}, PSEC 1.1={supports_1_1}, ingress={ingress_supported}" ); } } + #[test] + fn identityless_loopback_proxy_with_ingress_does_not_need_compatibility_queries() { + assert!( + BaseContainerRunner::identityless_loopback_proxy_supported(true, |_| { + panic!("ingress support is sufficient for an identity-less loopback proxy") + }) + .unwrap() + ); + } + + #[test] + fn identityless_loopback_proxy_propagates_version_query_errors() { + for failing_version in [ + SecurityEnvironmentVersion::V1_0, + SecurityEnvironmentVersion::V1_1, + ] { + let result = + BaseContainerRunner::identityless_loopback_proxy_supported(false, |version| { + if version == failing_version { + Err(LearningModeError::HResultCall { + function: "IsProcessSecurityEnvironmentVersionSupported", + code: E_NOTIMPL.0, + }) + } else { + Ok(false) + } + }); + assert!(matches!(result, Err(LearningModeError::HResultCall { .. }))); + } + } + #[test] fn psec_1_0_proxy_workaround_requires_a_1_0_only_host() { let request = psec_1_0_proxy_request(); diff --git a/src/mxc-sdk/src/backends/process_container/common/probe.rs b/src/mxc-sdk/src/backends/process_container/common/probe.rs index 8d73e7bd6..a6039ffcb 100644 --- a/src/mxc-sdk/src/backends/process_container/common/probe.rs +++ b/src/mxc-sdk/src/backends/process_container/common/probe.rs @@ -74,9 +74,9 @@ pub struct ProbeFacts { /// Whether BaseContainer can honor /// `network.ingress.hostLoopback = "allow"`. pub base_container_supports_ingress_host_loopback_allow: bool, - /// Whether the PSEC 1.0-only identity-less proxy loopback workaround is - /// available. Requires explicit host-loopback allow, not general ingress. - pub base_container_supports_proxy_loopback_compatibility: bool, + /// Whether BaseContainer supports an identity-less proxy on loopback. + /// Requires explicit host-loopback allow; not general ingress support. + pub base_container_supports_identityless_loopback_proxy: bool, /// Whether the in-proc IsolationSession service can be activated on this /// host. Always `false` here — `process_container_common` has no dependency on /// the isolation-session backend; `wxc-exec --probe` overrides it when @@ -140,7 +140,7 @@ impl From for UiCapabilitySupport { pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> ProbeOutput { use crate::process_container_common::base_container_runner::BaseContainerRunner; - let proxy_loopback_compatibility = BaseContainerRunner::supports_proxy_loopback_compatibility(); + let identityless_loopback_proxy = BaseContainerRunner::supports_identityless_loopback_proxy(); let probes = ProbeFacts { base_container_api_present: BaseContainerRunner::is_base_container_api_present(), native_capture_available: BaseContainerRunner::is_native_capture_available(), @@ -154,8 +154,8 @@ pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> base_container_supports_enumerate_paths: BaseContainerRunner::supports_enumerate_paths(), base_container_supports_ingress_host_loopback_allow: BaseContainerRunner::supports_ingress_host_loopback_allow(), - base_container_supports_proxy_loopback_compatibility: matches!( - proxy_loopback_compatibility, + base_container_supports_identityless_loopback_proxy: matches!( + identityless_loopback_proxy, Ok(true) ), isolation_session_available: false, @@ -169,9 +169,9 @@ pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> probes, fallback_detector::choose_backend_tier(request), ); - if let Err(error) = proxy_loopback_compatibility { + if let Err(error) = identityless_loopback_proxy { output.warnings.push(format!( - "failed to query PSEC proxy loopback compatibility support: {error}" + "failed to query identity-less loopback proxy support: {error}" )); } output @@ -283,7 +283,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, - base_container_supports_proxy_loopback_compatibility: false, + base_container_supports_identityless_loopback_proxy: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -305,7 +305,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, - base_container_supports_proxy_loopback_compatibility: true, + base_container_supports_identityless_loopback_proxy: true, isolation_session_available: true, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -329,7 +329,7 @@ mod tests { false ); assert_eq!( - v["probes"]["baseContainerSupportsProxyLoopbackCompatibility"], + v["probes"]["baseContainerSupportsIdentitylessLoopbackProxy"], true ); assert_eq!(v["probes"]["isolationSessionAvailable"], true); @@ -360,7 +360,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, - base_container_supports_proxy_loopback_compatibility: false, + base_container_supports_identityless_loopback_proxy: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: UiCapabilitySupport { diff --git a/src/mxc-sdk/src/core/mxc_engine/probe.rs b/src/mxc-sdk/src/core/mxc_engine/probe.rs index 64973ef9d..86ee60b36 100644 --- a/src/mxc-sdk/src/core/mxc_engine/probe.rs +++ b/src/mxc-sdk/src/core/mxc_engine/probe.rs @@ -30,9 +30,9 @@ pub enum BackendCapability { IngressHostLoopbackAllow, /// Bubblewrap proxy-only egress in a private network namespace. ProxyEnforcement, - /// Identity-less proxy loopback compatibility on PSEC 1.0-only hosts. + /// Identity-less proxy support on loopback. /// Requires explicit host-loopback allow; not general ingress support. - ProxyLoopbackCompatibility, + IdentitylessLoopbackProxy, } /// One host-available backend, plus its effective isolation tier (if any). @@ -110,10 +110,10 @@ pub fn available_backends() -> Vec { ), ..Default::default() }; - match BaseContainerRunner::supports_proxy_loopback_compatibility() { - Ok(supported) => support.proxy_loopback_compatibility = supported, + match BaseContainerRunner::supports_identityless_loopback_proxy() { + Ok(supported) => support.identityless_loopback_proxy = supported, Err(error) => support.warnings.push(format!( - "failed to query PSEC proxy loopback compatibility support: {error}" + "failed to query identity-less loopback proxy support: {error}" )), } windows_backends(tier, support) @@ -180,7 +180,7 @@ struct ProcessContainerCapabilities { filesystem_denied_paths: bool, filesystem_enumerate_paths: bool, ingress_host_loopback_allow: bool, - proxy_loopback_compatibility: bool, + identityless_loopback_proxy: bool, warnings: Vec, } @@ -210,8 +210,8 @@ fn windows_backends( if support.ingress_host_loopback_allow { capabilities.push(BackendCapability::IngressHostLoopbackAllow); } - if support.proxy_loopback_compatibility { - capabilities.push(BackendCapability::ProxyLoopbackCompatibility); + if support.identityless_loopback_proxy { + capabilities.push(BackendCapability::IdentitylessLoopbackProxy); } } let process_container = AvailableBackend { @@ -412,9 +412,9 @@ mod tests { r#""ingressHostLoopbackAllow""# ); assert_eq!( - serde_json::to_string(&BackendCapability::ProxyLoopbackCompatibility) + serde_json::to_string(&BackendCapability::IdentitylessLoopbackProxy) .expect("serializes"), - r#""proxyLoopbackCompatibility""# + r#""identitylessLoopbackProxy""# ); } @@ -550,7 +550,7 @@ mod tests { filesystem_denied_paths: true, filesystem_enumerate_paths: true, ingress_host_loopback_allow: true, - proxy_loopback_compatibility: true, + identityless_loopback_proxy: true, ..Default::default() }, ); @@ -565,14 +565,15 @@ mod tests { #[cfg(target_os = "windows")] #[test] - fn windows_reports_proxy_compatibility_without_general_ingress_support() { + fn windows_reports_identityless_proxy_with_or_without_general_ingress_support() { use crate::process_container_common::fallback_detector::IsolationTier; - for supported in [false, true] { + for (supported, ingress_supported) in [(false, false), (true, false), (true, true)] { let backends = windows_backends( IsolationTier::BaseContainer, ProcessContainerCapabilities { - proxy_loopback_compatibility: supported, + identityless_loopback_proxy: supported, + ingress_host_loopback_allow: ingress_supported, ..Default::default() }, ); @@ -580,12 +581,15 @@ mod tests { assert_eq!( process_container .capabilities - .contains(&BackendCapability::ProxyLoopbackCompatibility), + .contains(&BackendCapability::IdentitylessLoopbackProxy), supported ); - assert!(!process_container - .capabilities - .contains(&BackendCapability::IngressHostLoopbackAllow)); + assert_eq!( + process_container + .capabilities + .contains(&BackendCapability::IngressHostLoopbackAllow), + ingress_supported + ); } } @@ -594,7 +598,7 @@ mod tests { fn windows_preserves_capability_probe_warnings() { use crate::process_container_common::fallback_detector::IsolationTier; - let warning = "failed to query PSEC proxy loopback compatibility support".to_string(); + let warning = "failed to query identity-less loopback proxy support".to_string(); let backends = windows_backends( IsolationTier::BaseContainer, ProcessContainerCapabilities { @@ -605,7 +609,7 @@ mod tests { assert_eq!(backends[0].warnings, vec![warning]); assert!(!backends[0] .capabilities - .contains(&BackendCapability::ProxyLoopbackCompatibility)); + .contains(&BackendCapability::IdentitylessLoopbackProxy)); } #[cfg(target_os = "windows")] diff --git a/tests/scripts/README.md b/tests/scripts/README.md index 0ab02bd53..c0e2d04a0 100644 --- a/tests/scripts/README.md +++ b/tests/scripts/README.md @@ -60,10 +60,11 @@ built tree, which is the fastest way to iterate on one area: tests\scripts\run_processcontainer_network_proxy_test.ps1 -RequireTier base-container ``` -When the host probe reports `baseContainerSupportsProxyLoopbackCompatibility`, -the proxy area requires workload launch, the compatibility log, and a successful -proxied fetch. A clean policy rejection is a failure on these hosts. -Older binaries that omit this probe fact do not enable the compatibility-specific +When the host probe reports `baseContainerSupportsIdentitylessLoopbackProxy`, +the proxy area requires workload launch and a successful proxied fetch on both +PSEC 1.0-only hosts and hosts with PSEC 1.1 ingress support. +A clean policy rejection is a failure on these hosts. +Older binaries that omit this probe fact do not enable the capability-specific assertions; the existing proxy assertions still apply. Shared helpers live in `tests/scripts/lib/WinProcessContainer.Common.ps1`. It diff --git a/tests/scripts/lib/WinProcessContainer.Common.ps1 b/tests/scripts/lib/WinProcessContainer.Common.ps1 index b8e77ceb5..c28e82f11 100644 --- a/tests/scripts/lib/WinProcessContainer.Common.ps1 +++ b/tests/scripts/lib/WinProcessContainer.Common.ps1 @@ -351,8 +351,8 @@ function Get-HostCapabilities { $enumBit = if ($p.probes.PSObject.Properties['baseContainerSupportsEnumeratePaths']) { [bool]$p.probes.baseContainerSupportsEnumeratePaths } else { $false } - $proxyCompatibilityBit = if ($p.probes.PSObject.Properties['baseContainerSupportsProxyLoopbackCompatibility']) { - [bool]$p.probes.baseContainerSupportsProxyLoopbackCompatibility + $identitylessProxyBit = if ($p.probes.PSObject.Properties['baseContainerSupportsIdentitylessLoopbackProxy']) { + [bool]$p.probes.baseContainerSupportsIdentitylessLoopbackProxy } else { $false } # uiCapabilities is absent on older binaries / when the detector errored. $canInject = $false @@ -390,7 +390,7 @@ function Get-HostCapabilities { # denied tests auto-enable when it ships. SupportsDeniedPaths = (($tier -eq 'appcontainer-dacl') -or $denyBit) BaseContainerSupportsEnumeratePaths = $enumBit - BaseContainerSupportsProxyLoopbackCompatibility = $proxyCompatibilityBit + BaseContainerSupportsIdentitylessLoopbackProxy = $identitylessProxyBit # enumeratePaths has NO fallback: it needs PSEC 1.1 plus # PSE_SUPPORT_FS_ENUMERATE, and the detector refuses the request # outright on every AppContainer tier (FallbackError:: diff --git a/tests/scripts/run_processcontainer_network_proxy_test.ps1 b/tests/scripts/run_processcontainer_network_proxy_test.ps1 index b85b89ea8..59ce8799b 100644 --- a/tests/scripts/run_processcontainer_network_proxy_test.ps1 +++ b/tests/scripts/run_processcontainer_network_proxy_test.ps1 @@ -87,7 +87,7 @@ function Invoke-NetworkProxyAssertions { if ($psec) { $out = $envRun.Result.Stdout $ran = [bool]($out -match '(?im)^SystemRoot=') - $compatibilityAvailable = $Script:Caps.BaseContainerSupportsProxyLoopbackCompatibility + $identitylessProxyAvailable = $Script:Caps.BaseContainerSupportsIdentitylessLoopbackProxy # PSEC 1.0-only hosts use the explicit identity-less proxy workaround. # PSEC 1.1+ still requires the ingress support flag. A bare OS error # is never a valid substitute for successful execution or rejection. @@ -96,13 +96,11 @@ function Invoke-NetworkProxyAssertions { -Pass ($ran -or $rejectedCleanly) ` -Detail ("ran=$ran; rejectedAtValidation=$rejectedCleanly; exit=$($envRun.Result.ExitCode); " + "stderr=$(Format-Snippet $envRun.Result.Stderr)") - if ($compatibilityAvailable) { - $compatibilityLogged = [bool]($envRun.Log -match 'using PSEC 1\.0 proxy loopback compatibility') - Record-Result -Phase 'P8e' -Name 'PSEC 1.0-capable host launches and logs the proxy workaround' ` - -Pass ($ran -and $compatibilityLogged) ` - -Detail "exit=$($envRun.Result.ExitCode); ran=$ran; compatibilityLogged=$compatibilityLogged" + if ($identitylessProxyAvailable) { + Record-Result -Phase 'P8e' -Name 'identity-less loopback proxy-capable host launches the workload' ` + -Pass $ran -Detail "exit=$($envRun.Result.ExitCode); ran=$ran" } - if ($ran -or $compatibilityAvailable) { + if ($ran -or $identitylessProxyAvailable) { foreach ($v in @('HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy')) { # cmd.exe `set` upper-cases nothing, but Windows env lookup is # case-insensitive and duplicate-insensitive, so a variable set From e6604eb24a0475c254b82e9931fe36765ffb1833 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 19:58:28 -0300 Subject: [PATCH 08/10] fix(sdk): preserve published V1 probe facts Address review feedback confirmed against published Rust and Node 1.0.0 packages. Keep the existing ProbeFacts shape and expose identityless proxy support through backend discovery. Add source-compatibility regressions and keep capability-driven proxy checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cfef35-3eab-4f33-ba4c-3408938fc50d Signed-off-by: Carlos Alexandro Becker --- docs/api-reference/dotnet/v1/types.md | 1 - docs/api-reference/node/v1/types.md | 1 - docs/api-reference/rust/v1/types.md | 1 - docs/backends/process-container/networking.md | 9 ++-- .../Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs | 7 +-- .../V1/MxcContainerTests.cs | 36 --------------- .../Microsoft.Mxc.Sdk/MxcJsonSerialization.cs | 1 - .../Microsoft.Mxc.Sdk/V1/MxcContainer.cs | 2 - .../Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs | 6 --- sdk/node/src/v1/probe.ts | 1 - sdk/node/src/v1/types.ts | 2 - sdk/node/tests/unit/probe.test.ts | 32 +++---------- .../process_container/common/probe.rs | 25 +---------- src/mxc-sdk/tests/sdk_helpers.rs | 45 +++++++++++++++++++ tests/scripts/README.md | 4 +- .../lib/WinProcessContainer.Common.ps1 | 15 ++++--- 16 files changed, 70 insertions(+), 118 deletions(-) diff --git a/docs/api-reference/dotnet/v1/types.md b/docs/api-reference/dotnet/v1/types.md index 28ab98ff2..bf181f086 100644 --- a/docs/api-reference/dotnet/v1/types.md +++ b/docs/api-reference/dotnet/v1/types.md @@ -955,7 +955,6 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } - public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } diff --git a/docs/api-reference/node/v1/types.md b/docs/api-reference/node/v1/types.md index 83b11b3eb..50fa2e34d 100644 --- a/docs/api-reference/node/v1/types.md +++ b/docs/api-reference/node/v1/types.md @@ -579,7 +579,6 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; - baseContainerSupportsIdentitylessLoopbackProxy: boolean; isolationSessionAvailable: boolean; hyperlightAvailable: boolean; uiCapabilities: UiCapabilitySupport; diff --git a/docs/api-reference/rust/v1/types.md b/docs/api-reference/rust/v1/types.md index 56dafe324..8dddfb72a 100644 --- a/docs/api-reference/rust/v1/types.md +++ b/docs/api-reference/rust/v1/types.md @@ -536,7 +536,6 @@ pub struct ProbeFacts { pub base_container_supports_deny_paths: bool, pub base_container_supports_enumerate_paths: bool, pub base_container_supports_ingress_host_loopback_allow: bool, - pub base_container_supports_identityless_loopback_proxy: bool, pub isolation_session_available: bool, pub hyperlight_available: bool, pub ui_capabilities: UiCapabilitySupport, diff --git a/docs/backends/process-container/networking.md b/docs/backends/process-container/networking.md index 0c399440f..a888879e4 100644 --- a/docs/backends/process-container/networking.md +++ b/docs/backends/process-container/networking.md @@ -129,14 +129,15 @@ Requests the PSEC contract cannot preserve continue to an AppContainer tier. Backend discovery reports `identitylessLoopbackProxy` on the ProcessContainer `base-container` tier when the host supports an identity-less proxy on loopback. -The Windows request probe also reports -`probes.baseContainerSupportsIdentitylessLoopbackProxy`, including when called -without a request. Support is available on PSEC 1.0-only hosts and on hosts with +Use SDK backend discovery (`available_backends`, `MxcPlatform.GetAvailableBackends`, +or `getAvailableBackends`) or `wxc-exec --available-backends` to read it. +The published V1 request-probe result remains unchanged. +Support is available on PSEC 1.0-only hosts and on hosts with PSEC 1.1 or newer that advertise ingress support. A host that reports `baseContainerSupportsIngressHostLoopbackAllow` also supports an identity-less loopback proxy. PSEC 1.1 or newer without ingress support does not qualify, even if it also accepts PSEC 1.0 payloads. Version-query failures omit the -capability, set the probe fact to `false`, and add a warning. +capability and add a warning. This does not grant general `ingressHostLoopbackAllow` support or report that the current request uses an identity-less proxy. The request must set diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs index 4c8ffeb2a..c7e2b6ed6 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs @@ -337,8 +337,7 @@ static void CheckRoundTrip(T value, Func check, string name) "bfsCompiledIn": false, "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, - "baseContainerSupportsIngressHostLoopbackAllow": false, - "baseContainerSupportsIdentitylessLoopbackProxy": true, + "baseContainerSupportsIngressHostLoopbackAllow": true, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -360,11 +359,7 @@ static void CheckRoundTrip(T value, Func check, string name) Check(probe.Tier == IsolationTier.AppContainerDacl, "probe tier parsed"); Check(probe.NeedsDaclAugmentation == true, "probe dacl augmentation parsed"); Check(probe.Probes.BaseContainerApiPresent, "probe facts parsed"); -Check(probe.Probes.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy parsed"); Check(probe.Probes.UiCapabilities.CanBlockClipboardRead, "probe ui capabilities parsed"); -var nativeProbe = MxcJson.Deserialize(probeJson, MxcJson.ProbeOptions)!; -CheckRoundTrip(nativeProbe.Probes!, - x => x.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy facts"); var discovery = MxcPlatform.ParseAvailableBackends( """[{"backend":"processcontainer","tier":"base-container","capabilities":["identitylessLoopbackProxy"]}]"""); Check(discovery.Single().Capabilities.SequenceEqual([BackendCapability.IdentitylessLoopbackProxy]), diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs index 19587b073..cb5a82331 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs @@ -30,7 +30,6 @@ public class MxcContainerTests "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, "baseContainerSupportsIngressHostLoopbackAllow": true, - "baseContainerSupportsIdentitylessLoopbackProxy": true, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -273,41 +272,6 @@ public void ProbeParser_RejectsMissingRequiredProbeBoolean() json["probes"]!.AsObject().Remove("bfscfgPresent")); } - [Theory] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(true, true)] - public void ProbeParser_PreservesIdentitylessLoopbackProxy(bool supported, bool ingressSupported) - { - var json = CreateCompleteProbeJson(); - json["tier"] = "base-container"; - json["needsDaclAugmentation"] = false; - json["probes"]!["baseContainerSupportsIngressHostLoopbackAllow"] = ingressSupported; - json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = supported; - - var output = MxcContainer.ParseProbeOutput(json.ToJsonString()); - - Assert.Equal(supported, output.Probes.BaseContainerSupportsIdentitylessLoopbackProxy); - Assert.Equal(ingressSupported, output.Probes.BaseContainerSupportsIngressHostLoopbackAllow); - } - - [Fact] - public void ProbeParser_RejectsMissingIdentitylessLoopbackProxy() - { - AssertProbeJsonRejected(json => - json["probes"]!.AsObject().Remove("baseContainerSupportsIdentitylessLoopbackProxy")); - } - - [Theory] - [InlineData("null")] - [InlineData("\"true\"")] - [InlineData("1")] - public void ProbeParser_RejectsInvalidIdentitylessLoopbackProxy(string value) - { - AssertProbeJsonRejected(json => - json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = JsonNode.Parse(value)); - } - [Fact] public void ProbeParser_RejectsMissingRequiredUiBoolean() { diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs index d6eb9acf0..beb2b5147 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs @@ -82,7 +82,6 @@ public CamelCaseJsonStringEnumConverter() [JsonSerializable(typeof(NativeAvailableBackend[]))] [JsonSerializable(typeof(NativePlatformSupport))] [JsonSerializable(typeof(NativeProbeOutput))] -[JsonSerializable(typeof(NativeProbeFacts))] [JsonSerializable(typeof(ExecutionMetadata))] [JsonSerializable(typeof(CaptureDenialsResult))] [JsonSerializable(typeof(CaptureDenialsError))] diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs index 611da275f..07b37d411 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs @@ -177,8 +177,6 @@ internal static ProbeOutput ParseProbeOutput(string json) probes.BaseContainerSupportsEnumeratePaths, BaseContainerSupportsIngressHostLoopbackAllow = probes.BaseContainerSupportsIngressHostLoopbackAllow, - BaseContainerSupportsIdentitylessLoopbackProxy = - probes.BaseContainerSupportsIdentitylessLoopbackProxy, IsolationSessionAvailable = probes.IsolationSessionAvailable, HyperlightAvailable = probes.HyperlightAvailable, UiCapabilities = new UiCapabilitySupport diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs index b7eed9cfc..5205ebe11 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs @@ -152,8 +152,6 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } - /// Identity-less proxy support on loopback; requires explicit host-loopback allow. - public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } @@ -287,10 +285,6 @@ internal sealed class NativeProbeFacts [JsonPropertyName("baseContainerSupportsIngressHostLoopbackAllow")] public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } - [JsonRequired] - [JsonPropertyName("baseContainerSupportsIdentitylessLoopbackProxy")] - public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } - [JsonRequired] [JsonPropertyName("isolationSessionAvailable")] public bool IsolationSessionAvailable { get; init; } diff --git a/sdk/node/src/v1/probe.ts b/sdk/node/src/v1/probe.ts index e16a603e2..b6b7b9a34 100644 --- a/sdk/node/src/v1/probe.ts +++ b/sdk/node/src/v1/probe.ts @@ -31,7 +31,6 @@ const probeFactBooleanFields: readonly (keyof ProbeFacts)[] = [ 'baseContainerSupportsDenyPaths', 'baseContainerSupportsEnumeratePaths', 'baseContainerSupportsIngressHostLoopbackAllow', - 'baseContainerSupportsIdentitylessLoopbackProxy', 'isolationSessionAvailable', 'hyperlightAvailable', ]; diff --git a/sdk/node/src/v1/types.ts b/sdk/node/src/v1/types.ts index d8ba22dc6..eac5242ab 100644 --- a/sdk/node/src/v1/types.ts +++ b/sdk/node/src/v1/types.ts @@ -785,8 +785,6 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; - /** Identity-less proxy support on loopback; requires explicit host-loopback allow. */ - baseContainerSupportsIdentitylessLoopbackProxy: boolean; /** True when this executor includes IsolationSession and the host can activate it. */ isolationSessionAvailable: boolean; /** True when this executor includes Hyperlight and its host runtime is available. */ diff --git a/sdk/node/tests/unit/probe.test.ts b/sdk/node/tests/unit/probe.test.ts index ddddd93ee..9ecd8dc60 100644 --- a/sdk/node/tests/unit/probe.test.ts +++ b/sdk/node/tests/unit/probe.test.ts @@ -16,6 +16,7 @@ import { _setRequestProbeDependencies, probe, } from '../../src/v1/probe.js'; +import type { ProbeFacts } from '../../src/v1/types.js'; const completeProbe = { tier: 'appcontainer-dacl', @@ -30,7 +31,6 @@ const completeProbe = { baseContainerSupportsDenyPaths: false, baseContainerSupportsEnumeratePaths: false, baseContainerSupportsIngressHostLoopbackAllow: false, - baseContainerSupportsIdentitylessLoopbackProxy: false, isolationSessionAvailable: false, hyperlightAvailable: false, uiCapabilities: { @@ -193,33 +193,11 @@ describe('probe', () => { assert.equal(output.needsDaclAugmentation, true); }); - it('preserves identity-less proxy support with or without general ingress', () => { - for (const [supported, ingressSupported] of [[false, false], [true, false], [true, true]]) { - const output = runProbeOutput({ - ...completeProbe, - tier: 'base-container', - needsDaclAugmentation: false, - probes: { - ...completeProbe.probes, - baseContainerSupportsIdentitylessLoopbackProxy: supported, - baseContainerSupportsIngressHostLoopbackAllow: ingressSupported, - }, - }); - assert.equal(output.probes.baseContainerSupportsIdentitylessLoopbackProxy, supported); - assert.equal(output.probes.baseContainerSupportsIngressHostLoopbackAllow, ingressSupported); - } - }); + it('accepts the published V1 ProbeFacts shape', () => { + const facts: ProbeFacts = completeProbe.probes; + const output = runProbeOutput({ ...completeProbe, probes: facts }); - it('rejects missing or invalid identity-less loopback proxy facts', () => { - for (const value of [undefined, null, 'true', 1]) { - assertProbeOutputRejected({ - ...completeProbe, - probes: { - ...completeProbe.probes, - baseContainerSupportsIdentitylessLoopbackProxy: value, - }, - }); - } + assert.deepStrictEqual(output.probes, facts); }); it('rejects an unknown envelope field', () => { diff --git a/src/mxc-sdk/src/backends/process_container/common/probe.rs b/src/mxc-sdk/src/backends/process_container/common/probe.rs index a6039ffcb..7cdc519eb 100644 --- a/src/mxc-sdk/src/backends/process_container/common/probe.rs +++ b/src/mxc-sdk/src/backends/process_container/common/probe.rs @@ -74,9 +74,6 @@ pub struct ProbeFacts { /// Whether BaseContainer can honor /// `network.ingress.hostLoopback = "allow"`. pub base_container_supports_ingress_host_loopback_allow: bool, - /// Whether BaseContainer supports an identity-less proxy on loopback. - /// Requires explicit host-loopback allow; not general ingress support. - pub base_container_supports_identityless_loopback_proxy: bool, /// Whether the in-proc IsolationSession service can be activated on this /// host. Always `false` here — `process_container_common` has no dependency on /// the isolation-session backend; `wxc-exec --probe` overrides it when @@ -140,7 +137,6 @@ impl From for UiCapabilitySupport { pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> ProbeOutput { use crate::process_container_common::base_container_runner::BaseContainerRunner; - let identityless_loopback_proxy = BaseContainerRunner::supports_identityless_loopback_proxy(); let probes = ProbeFacts { base_container_api_present: BaseContainerRunner::is_base_container_api_present(), native_capture_available: BaseContainerRunner::is_native_capture_available(), @@ -154,27 +150,17 @@ pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> base_container_supports_enumerate_paths: BaseContainerRunner::supports_enumerate_paths(), base_container_supports_ingress_host_loopback_allow: BaseContainerRunner::supports_ingress_host_loopback_allow(), - base_container_supports_identityless_loopback_proxy: matches!( - identityless_loopback_proxy, - Ok(true) - ), isolation_session_available: false, hyperlight_available: false, ui_capabilities: crate::process_container_common::job_object::supported_ui_restrictions() .into(), }; - let mut output = run_probe_with_tier_decision( + run_probe_with_tier_decision( request, probes, fallback_detector::choose_backend_tier(request), - ); - if let Err(error) = identityless_loopback_proxy { - output.warnings.push(format!( - "failed to query identity-less loopback proxy support: {error}" - )); - } - output + ) } fn run_probe_with_tier_decision( @@ -283,7 +269,6 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, - base_container_supports_identityless_loopback_proxy: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -305,7 +290,6 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, - base_container_supports_identityless_loopback_proxy: true, isolation_session_available: true, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -328,10 +312,6 @@ mod tests { v["probes"]["baseContainerSupportsIngressHostLoopbackAllow"], false ); - assert_eq!( - v["probes"]["baseContainerSupportsIdentitylessLoopbackProxy"], - true - ); assert_eq!(v["probes"]["isolationSessionAvailable"], true); assert_eq!(v["probes"]["uiCapabilities"]["canBlockClipboardRead"], true); assert_eq!( @@ -360,7 +340,6 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, - base_container_supports_identityless_loopback_proxy: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: UiCapabilitySupport { diff --git a/src/mxc-sdk/tests/sdk_helpers.rs b/src/mxc-sdk/tests/sdk_helpers.rs index 35b897f33..7e920049d 100644 --- a/src/mxc-sdk/tests/sdk_helpers.rs +++ b/src/mxc-sdk/tests/sdk_helpers.rs @@ -34,6 +34,51 @@ fn platform_support_reports_host() { assert!(!support.available_methods.is_empty()); } +#[cfg(target_os = "windows")] +#[test] +fn published_v1_probe_facts_remain_constructible_and_exhaustive() { + use mxc_sdk::v1::{ProbeFacts, UiCapabilitySupport}; + + let facts = ProbeFacts { + base_container_api_present: false, + native_capture_available: false, + guarded_capture_available: false, + bfscfg_present: false, + bfs_compiled_in: false, + base_container_supports_deny_paths: false, + base_container_supports_enumerate_paths: false, + base_container_supports_ingress_host_loopback_allow: false, + isolation_session_available: false, + hyperlight_available: false, + ui_capabilities: UiCapabilitySupport { + can_block_clipboard_read: false, + can_block_clipboard_write: false, + can_block_input_injection: false, + can_block_input_method_changes: false, + can_block_external_ui_objects: false, + can_block_global_ui_namespace: false, + can_block_desktop_switching: false, + can_block_logoff_or_shutdown: false, + can_block_system_parameter_changes: false, + can_block_display_settings_changes: false, + }, + }; + let ProbeFacts { + base_container_api_present, + native_capture_available: _, + guarded_capture_available: _, + bfscfg_present: _, + bfs_compiled_in: _, + base_container_supports_deny_paths: _, + base_container_supports_enumerate_paths: _, + base_container_supports_ingress_host_loopback_allow: _, + isolation_session_available: _, + hyperlight_available: _, + ui_capabilities: _, + } = facts; + assert!(!base_container_api_present); +} + #[cfg(target_os = "macos")] #[test] fn platform_support_macos_is_seatbelt() { diff --git a/tests/scripts/README.md b/tests/scripts/README.md index c0e2d04a0..a5efb1dff 100644 --- a/tests/scripts/README.md +++ b/tests/scripts/README.md @@ -60,11 +60,11 @@ built tree, which is the fastest way to iterate on one area: tests\scripts\run_processcontainer_network_proxy_test.ps1 -RequireTier base-container ``` -When the host probe reports `baseContainerSupportsIdentitylessLoopbackProxy`, +When backend discovery reports the `identitylessLoopbackProxy` capability, the proxy area requires workload launch and a successful proxied fetch on both PSEC 1.0-only hosts and hosts with PSEC 1.1 ingress support. A clean policy rejection is a failure on these hosts. -Older binaries that omit this probe fact do not enable the capability-specific +Older binaries that omit this capability do not enable the capability-specific assertions; the existing proxy assertions still apply. Shared helpers live in `tests/scripts/lib/WinProcessContainer.Common.ps1`. It diff --git a/tests/scripts/lib/WinProcessContainer.Common.ps1 b/tests/scripts/lib/WinProcessContainer.Common.ps1 index 28dfab954..e7f9bcc97 100644 --- a/tests/scripts/lib/WinProcessContainer.Common.ps1 +++ b/tests/scripts/lib/WinProcessContainer.Common.ps1 @@ -351,9 +351,13 @@ function Get-HostCapabilities { $enumBit = if ($p.probes.PSObject.Properties['baseContainerSupportsEnumeratePaths']) { [bool]$p.probes.baseContainerSupportsEnumeratePaths } else { $false } - $identitylessProxyBit = if ($p.probes.PSObject.Properties['baseContainerSupportsIdentitylessLoopbackProxy']) { - [bool]$p.probes.baseContainerSupportsIdentitylessLoopbackProxy - } else { $false } + $backends = Invoke-Probe -Wxc $WxcDebug -AvailableBackends -Phase 'P0' -Name 'backend-capabilities' + $processContainer = @($backends | Where-Object { $_ -and $_.backend -eq 'processcontainer' }) + if ($processContainer.Count -ne 1) { + throw 'Get-HostCapabilities: backend discovery did not report exactly one ProcessContainer backend.' + } + $identitylessProxyBit = $processContainer[0].PSObject.Properties['capabilities'] -and + ($processContainer[0].capabilities -contains 'identitylessLoopbackProxy') # uiCapabilities is absent on older binaries / when the detector errored. $canInject = $false if ($p.probes.PSObject.Properties['uiCapabilities'] -and @@ -694,12 +698,13 @@ function New-Config { # Test runners function Invoke-Probe { - param([string]$Wxc, [string]$ConfigPath = $null, [string]$Phase, [string]$Name) + param([string]$Wxc, [string]$ConfigPath = $null, [string]$Phase, [string]$Name, + [switch]$AvailableBackends) # Use ProcessStartInfo so we can keep stdout (the JSON) separate from # stderr (DACL-recovery messages, build-time warnings). $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Wxc - $argList = @('--probe') + $argList = @(if ($AvailableBackends) { '--available-backends' } else { '--probe' }) if ($ConfigPath) { $argList += @('--config', "`"$ConfigPath`"") } $psi.Arguments = ($argList -join ' ') $psi.RedirectStandardOutput = $true From 5557e86be531201e150ffe0c73f0d199c97db20e Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 19:58:28 -0300 Subject: [PATCH 09/10] docs(network): correct identityless proxy host requirements Address review feedback: document PSEC 1.0-only support and retain the ingress-flag requirement on PSEC 1.1 and newer hosts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cfef35-3eab-4f33-ba4c-3408938fc50d Signed-off-by: Carlos Alexandro Becker --- docs/examples.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/examples.md b/docs/examples.md index 00c1aee31..63c34d428 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -135,9 +135,11 @@ the development/testing configuration is: This identity-less host-loopback deployment restricts client egress to the configured proxy address and port, but does not verify which process owns that -endpoint. It requires native PSEC 1.1 ingress/host-loopback support; unsupported -hosts reject the request. For production ProcessContainer deployments, identify -a packaged proxy through `processContainer.network.allowedProxyPeer` instead; see +endpoint. It supports PSEC 1.0-only hosts and hosts with PSEC 1.1 or newer that +advertise ingress support. PSEC 1.1 or newer without that flag rejects the +request, even if the host also accepts PSEC 1.0 payloads. For production +ProcessContainer deployments, identify a packaged proxy through +`processContainer.network.allowedProxyPeer` instead; see [proxy deployment choices](backends/process-container/networking.md#proxy-deployment-choices). Bubblewrap and Seatbelt also support a caller-managed loopback proxy, but their supported ingress policies differ. See their backend guides. From ce9bb314300afe50652893e6dd86438b3db67521 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 6 Oct 2026 20:07:39 -0300 Subject: [PATCH 10/10] revert: restore identityless loopback proxy probe fact This reverts commit e6604eb24a0475c254b82e9931fe36765ffb1833 at the author's request. Keep the output-only probe fact and preserve the later example documentation correction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19cfef35-3eab-4f33-ba4c-3408938fc50d Signed-off-by: Carlos Alexandro Becker --- docs/api-reference/dotnet/v1/types.md | 1 + docs/api-reference/node/v1/types.md | 1 + docs/api-reference/rust/v1/types.md | 1 + docs/backends/process-container/networking.md | 9 ++-- .../Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs | 7 ++- .../V1/MxcContainerTests.cs | 36 +++++++++++++++ .../Microsoft.Mxc.Sdk/MxcJsonSerialization.cs | 1 + .../Microsoft.Mxc.Sdk/V1/MxcContainer.cs | 2 + .../Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs | 6 +++ sdk/node/src/v1/probe.ts | 1 + sdk/node/src/v1/types.ts | 2 + sdk/node/tests/unit/probe.test.ts | 32 ++++++++++--- .../process_container/common/probe.rs | 25 ++++++++++- src/mxc-sdk/tests/sdk_helpers.rs | 45 ------------------- tests/scripts/README.md | 4 +- .../lib/WinProcessContainer.Common.ps1 | 15 +++---- 16 files changed, 118 insertions(+), 70 deletions(-) diff --git a/docs/api-reference/dotnet/v1/types.md b/docs/api-reference/dotnet/v1/types.md index bf181f086..28ab98ff2 100644 --- a/docs/api-reference/dotnet/v1/types.md +++ b/docs/api-reference/dotnet/v1/types.md @@ -955,6 +955,7 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } + public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } diff --git a/docs/api-reference/node/v1/types.md b/docs/api-reference/node/v1/types.md index 50fa2e34d..83b11b3eb 100644 --- a/docs/api-reference/node/v1/types.md +++ b/docs/api-reference/node/v1/types.md @@ -579,6 +579,7 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; + baseContainerSupportsIdentitylessLoopbackProxy: boolean; isolationSessionAvailable: boolean; hyperlightAvailable: boolean; uiCapabilities: UiCapabilitySupport; diff --git a/docs/api-reference/rust/v1/types.md b/docs/api-reference/rust/v1/types.md index 8dddfb72a..56dafe324 100644 --- a/docs/api-reference/rust/v1/types.md +++ b/docs/api-reference/rust/v1/types.md @@ -536,6 +536,7 @@ pub struct ProbeFacts { pub base_container_supports_deny_paths: bool, pub base_container_supports_enumerate_paths: bool, pub base_container_supports_ingress_host_loopback_allow: bool, + pub base_container_supports_identityless_loopback_proxy: bool, pub isolation_session_available: bool, pub hyperlight_available: bool, pub ui_capabilities: UiCapabilitySupport, diff --git a/docs/backends/process-container/networking.md b/docs/backends/process-container/networking.md index a888879e4..0c399440f 100644 --- a/docs/backends/process-container/networking.md +++ b/docs/backends/process-container/networking.md @@ -129,15 +129,14 @@ Requests the PSEC contract cannot preserve continue to an AppContainer tier. Backend discovery reports `identitylessLoopbackProxy` on the ProcessContainer `base-container` tier when the host supports an identity-less proxy on loopback. -Use SDK backend discovery (`available_backends`, `MxcPlatform.GetAvailableBackends`, -or `getAvailableBackends`) or `wxc-exec --available-backends` to read it. -The published V1 request-probe result remains unchanged. -Support is available on PSEC 1.0-only hosts and on hosts with +The Windows request probe also reports +`probes.baseContainerSupportsIdentitylessLoopbackProxy`, including when called +without a request. Support is available on PSEC 1.0-only hosts and on hosts with PSEC 1.1 or newer that advertise ingress support. A host that reports `baseContainerSupportsIngressHostLoopbackAllow` also supports an identity-less loopback proxy. PSEC 1.1 or newer without ingress support does not qualify, even if it also accepts PSEC 1.0 payloads. Version-query failures omit the -capability and add a warning. +capability, set the probe fact to `false`, and add a warning. This does not grant general `ingressHostLoopbackAllow` support or report that the current request uses an identity-less proxy. The request must set diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs index c7e2b6ed6..4c8ffeb2a 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.AotSmokeTest/Program.cs @@ -337,7 +337,8 @@ static void CheckRoundTrip(T value, Func check, string name) "bfsCompiledIn": false, "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, - "baseContainerSupportsIngressHostLoopbackAllow": true, + "baseContainerSupportsIngressHostLoopbackAllow": false, + "baseContainerSupportsIdentitylessLoopbackProxy": true, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -359,7 +360,11 @@ static void CheckRoundTrip(T value, Func check, string name) Check(probe.Tier == IsolationTier.AppContainerDacl, "probe tier parsed"); Check(probe.NeedsDaclAugmentation == true, "probe dacl augmentation parsed"); Check(probe.Probes.BaseContainerApiPresent, "probe facts parsed"); +Check(probe.Probes.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy parsed"); Check(probe.Probes.UiCapabilities.CanBlockClipboardRead, "probe ui capabilities parsed"); +var nativeProbe = MxcJson.Deserialize(probeJson, MxcJson.ProbeOptions)!; +CheckRoundTrip(nativeProbe.Probes!, + x => x.BaseContainerSupportsIdentitylessLoopbackProxy, "identity-less loopback proxy facts"); var discovery = MxcPlatform.ParseAvailableBackends( """[{"backend":"processcontainer","tier":"base-container","capabilities":["identitylessLoopbackProxy"]}]"""); Check(discovery.Single().Capabilities.SequenceEqual([BackendCapability.IdentitylessLoopbackProxy]), diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs index cb5a82331..19587b073 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/V1/MxcContainerTests.cs @@ -30,6 +30,7 @@ public class MxcContainerTests "baseContainerSupportsDenyPaths": true, "baseContainerSupportsEnumeratePaths": false, "baseContainerSupportsIngressHostLoopbackAllow": true, + "baseContainerSupportsIdentitylessLoopbackProxy": true, "isolationSessionAvailable": true, "hyperlightAvailable": false, "uiCapabilities": { @@ -272,6 +273,41 @@ public void ProbeParser_RejectsMissingRequiredProbeBoolean() json["probes"]!.AsObject().Remove("bfscfgPresent")); } + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void ProbeParser_PreservesIdentitylessLoopbackProxy(bool supported, bool ingressSupported) + { + var json = CreateCompleteProbeJson(); + json["tier"] = "base-container"; + json["needsDaclAugmentation"] = false; + json["probes"]!["baseContainerSupportsIngressHostLoopbackAllow"] = ingressSupported; + json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = supported; + + var output = MxcContainer.ParseProbeOutput(json.ToJsonString()); + + Assert.Equal(supported, output.Probes.BaseContainerSupportsIdentitylessLoopbackProxy); + Assert.Equal(ingressSupported, output.Probes.BaseContainerSupportsIngressHostLoopbackAllow); + } + + [Fact] + public void ProbeParser_RejectsMissingIdentitylessLoopbackProxy() + { + AssertProbeJsonRejected(json => + json["probes"]!.AsObject().Remove("baseContainerSupportsIdentitylessLoopbackProxy")); + } + + [Theory] + [InlineData("null")] + [InlineData("\"true\"")] + [InlineData("1")] + public void ProbeParser_RejectsInvalidIdentitylessLoopbackProxy(string value) + { + AssertProbeJsonRejected(json => + json["probes"]!["baseContainerSupportsIdentitylessLoopbackProxy"] = JsonNode.Parse(value)); + } + [Fact] public void ProbeParser_RejectsMissingRequiredUiBoolean() { diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs index beb2b5147..d6eb9acf0 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcJsonSerialization.cs @@ -82,6 +82,7 @@ public CamelCaseJsonStringEnumConverter() [JsonSerializable(typeof(NativeAvailableBackend[]))] [JsonSerializable(typeof(NativePlatformSupport))] [JsonSerializable(typeof(NativeProbeOutput))] +[JsonSerializable(typeof(NativeProbeFacts))] [JsonSerializable(typeof(ExecutionMetadata))] [JsonSerializable(typeof(CaptureDenialsResult))] [JsonSerializable(typeof(CaptureDenialsError))] diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs index 07b37d411..611da275f 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/MxcContainer.cs @@ -177,6 +177,8 @@ internal static ProbeOutput ParseProbeOutput(string json) probes.BaseContainerSupportsEnumeratePaths, BaseContainerSupportsIngressHostLoopbackAllow = probes.BaseContainerSupportsIngressHostLoopbackAllow, + BaseContainerSupportsIdentitylessLoopbackProxy = + probes.BaseContainerSupportsIdentitylessLoopbackProxy, IsolationSessionAvailable = probes.IsolationSessionAvailable, HyperlightAvailable = probes.HyperlightAvailable, UiCapabilities = new UiCapabilitySupport diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs index 5205ebe11..b7eed9cfc 100644 --- a/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs +++ b/sdk/dotnet/Microsoft.Mxc.Sdk/V1/PlatformDiscovery.cs @@ -152,6 +152,8 @@ public sealed class ProbeFacts public bool BaseContainerSupportsDenyPaths { get; init; } public bool BaseContainerSupportsEnumeratePaths { get; init; } public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } + /// Identity-less proxy support on loopback; requires explicit host-loopback allow. + public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } public bool IsolationSessionAvailable { get; init; } public bool HyperlightAvailable { get; init; } public required UiCapabilitySupport UiCapabilities { get; init; } @@ -285,6 +287,10 @@ internal sealed class NativeProbeFacts [JsonPropertyName("baseContainerSupportsIngressHostLoopbackAllow")] public bool BaseContainerSupportsIngressHostLoopbackAllow { get; init; } + [JsonRequired] + [JsonPropertyName("baseContainerSupportsIdentitylessLoopbackProxy")] + public bool BaseContainerSupportsIdentitylessLoopbackProxy { get; init; } + [JsonRequired] [JsonPropertyName("isolationSessionAvailable")] public bool IsolationSessionAvailable { get; init; } diff --git a/sdk/node/src/v1/probe.ts b/sdk/node/src/v1/probe.ts index b6b7b9a34..e16a603e2 100644 --- a/sdk/node/src/v1/probe.ts +++ b/sdk/node/src/v1/probe.ts @@ -31,6 +31,7 @@ const probeFactBooleanFields: readonly (keyof ProbeFacts)[] = [ 'baseContainerSupportsDenyPaths', 'baseContainerSupportsEnumeratePaths', 'baseContainerSupportsIngressHostLoopbackAllow', + 'baseContainerSupportsIdentitylessLoopbackProxy', 'isolationSessionAvailable', 'hyperlightAvailable', ]; diff --git a/sdk/node/src/v1/types.ts b/sdk/node/src/v1/types.ts index eac5242ab..d8ba22dc6 100644 --- a/sdk/node/src/v1/types.ts +++ b/sdk/node/src/v1/types.ts @@ -785,6 +785,8 @@ export interface ProbeFacts { baseContainerSupportsDenyPaths: boolean; baseContainerSupportsEnumeratePaths: boolean; baseContainerSupportsIngressHostLoopbackAllow: boolean; + /** Identity-less proxy support on loopback; requires explicit host-loopback allow. */ + baseContainerSupportsIdentitylessLoopbackProxy: boolean; /** True when this executor includes IsolationSession and the host can activate it. */ isolationSessionAvailable: boolean; /** True when this executor includes Hyperlight and its host runtime is available. */ diff --git a/sdk/node/tests/unit/probe.test.ts b/sdk/node/tests/unit/probe.test.ts index 9ecd8dc60..ddddd93ee 100644 --- a/sdk/node/tests/unit/probe.test.ts +++ b/sdk/node/tests/unit/probe.test.ts @@ -16,7 +16,6 @@ import { _setRequestProbeDependencies, probe, } from '../../src/v1/probe.js'; -import type { ProbeFacts } from '../../src/v1/types.js'; const completeProbe = { tier: 'appcontainer-dacl', @@ -31,6 +30,7 @@ const completeProbe = { baseContainerSupportsDenyPaths: false, baseContainerSupportsEnumeratePaths: false, baseContainerSupportsIngressHostLoopbackAllow: false, + baseContainerSupportsIdentitylessLoopbackProxy: false, isolationSessionAvailable: false, hyperlightAvailable: false, uiCapabilities: { @@ -193,11 +193,33 @@ describe('probe', () => { assert.equal(output.needsDaclAugmentation, true); }); - it('accepts the published V1 ProbeFacts shape', () => { - const facts: ProbeFacts = completeProbe.probes; - const output = runProbeOutput({ ...completeProbe, probes: facts }); + it('preserves identity-less proxy support with or without general ingress', () => { + for (const [supported, ingressSupported] of [[false, false], [true, false], [true, true]]) { + const output = runProbeOutput({ + ...completeProbe, + tier: 'base-container', + needsDaclAugmentation: false, + probes: { + ...completeProbe.probes, + baseContainerSupportsIdentitylessLoopbackProxy: supported, + baseContainerSupportsIngressHostLoopbackAllow: ingressSupported, + }, + }); + assert.equal(output.probes.baseContainerSupportsIdentitylessLoopbackProxy, supported); + assert.equal(output.probes.baseContainerSupportsIngressHostLoopbackAllow, ingressSupported); + } + }); - assert.deepStrictEqual(output.probes, facts); + it('rejects missing or invalid identity-less loopback proxy facts', () => { + for (const value of [undefined, null, 'true', 1]) { + assertProbeOutputRejected({ + ...completeProbe, + probes: { + ...completeProbe.probes, + baseContainerSupportsIdentitylessLoopbackProxy: value, + }, + }); + } }); it('rejects an unknown envelope field', () => { diff --git a/src/mxc-sdk/src/backends/process_container/common/probe.rs b/src/mxc-sdk/src/backends/process_container/common/probe.rs index 7cdc519eb..a6039ffcb 100644 --- a/src/mxc-sdk/src/backends/process_container/common/probe.rs +++ b/src/mxc-sdk/src/backends/process_container/common/probe.rs @@ -74,6 +74,9 @@ pub struct ProbeFacts { /// Whether BaseContainer can honor /// `network.ingress.hostLoopback = "allow"`. pub base_container_supports_ingress_host_loopback_allow: bool, + /// Whether BaseContainer supports an identity-less proxy on loopback. + /// Requires explicit host-loopback allow; not general ingress support. + pub base_container_supports_identityless_loopback_proxy: bool, /// Whether the in-proc IsolationSession service can be activated on this /// host. Always `false` here — `process_container_common` has no dependency on /// the isolation-session backend; `wxc-exec --probe` overrides it when @@ -137,6 +140,7 @@ impl From for UiCapabilitySupport { pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> ProbeOutput { use crate::process_container_common::base_container_runner::BaseContainerRunner; + let identityless_loopback_proxy = BaseContainerRunner::supports_identityless_loopback_proxy(); let probes = ProbeFacts { base_container_api_present: BaseContainerRunner::is_base_container_api_present(), native_capture_available: BaseContainerRunner::is_native_capture_available(), @@ -150,17 +154,27 @@ pub fn run_probe(request: &ExecutionRequest, guarded_capture_available: bool) -> base_container_supports_enumerate_paths: BaseContainerRunner::supports_enumerate_paths(), base_container_supports_ingress_host_loopback_allow: BaseContainerRunner::supports_ingress_host_loopback_allow(), + base_container_supports_identityless_loopback_proxy: matches!( + identityless_loopback_proxy, + Ok(true) + ), isolation_session_available: false, hyperlight_available: false, ui_capabilities: crate::process_container_common::job_object::supported_ui_restrictions() .into(), }; - run_probe_with_tier_decision( + let mut output = run_probe_with_tier_decision( request, probes, fallback_detector::choose_backend_tier(request), - ) + ); + if let Err(error) = identityless_loopback_proxy { + output.warnings.push(format!( + "failed to query identity-less loopback proxy support: {error}" + )); + } + output } fn run_probe_with_tier_decision( @@ -269,6 +283,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, + base_container_supports_identityless_loopback_proxy: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -290,6 +305,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, + base_container_supports_identityless_loopback_proxy: true, isolation_session_available: true, hyperlight_available: false, ui_capabilities: all_ui_capabilities(), @@ -312,6 +328,10 @@ mod tests { v["probes"]["baseContainerSupportsIngressHostLoopbackAllow"], false ); + assert_eq!( + v["probes"]["baseContainerSupportsIdentitylessLoopbackProxy"], + true + ); assert_eq!(v["probes"]["isolationSessionAvailable"], true); assert_eq!(v["probes"]["uiCapabilities"]["canBlockClipboardRead"], true); assert_eq!( @@ -340,6 +360,7 @@ mod tests { base_container_supports_deny_paths: false, base_container_supports_enumerate_paths: false, base_container_supports_ingress_host_loopback_allow: false, + base_container_supports_identityless_loopback_proxy: false, isolation_session_available: false, hyperlight_available: false, ui_capabilities: UiCapabilitySupport { diff --git a/src/mxc-sdk/tests/sdk_helpers.rs b/src/mxc-sdk/tests/sdk_helpers.rs index 7e920049d..35b897f33 100644 --- a/src/mxc-sdk/tests/sdk_helpers.rs +++ b/src/mxc-sdk/tests/sdk_helpers.rs @@ -34,51 +34,6 @@ fn platform_support_reports_host() { assert!(!support.available_methods.is_empty()); } -#[cfg(target_os = "windows")] -#[test] -fn published_v1_probe_facts_remain_constructible_and_exhaustive() { - use mxc_sdk::v1::{ProbeFacts, UiCapabilitySupport}; - - let facts = ProbeFacts { - base_container_api_present: false, - native_capture_available: false, - guarded_capture_available: false, - bfscfg_present: false, - bfs_compiled_in: false, - base_container_supports_deny_paths: false, - base_container_supports_enumerate_paths: false, - base_container_supports_ingress_host_loopback_allow: false, - isolation_session_available: false, - hyperlight_available: false, - ui_capabilities: UiCapabilitySupport { - can_block_clipboard_read: false, - can_block_clipboard_write: false, - can_block_input_injection: false, - can_block_input_method_changes: false, - can_block_external_ui_objects: false, - can_block_global_ui_namespace: false, - can_block_desktop_switching: false, - can_block_logoff_or_shutdown: false, - can_block_system_parameter_changes: false, - can_block_display_settings_changes: false, - }, - }; - let ProbeFacts { - base_container_api_present, - native_capture_available: _, - guarded_capture_available: _, - bfscfg_present: _, - bfs_compiled_in: _, - base_container_supports_deny_paths: _, - base_container_supports_enumerate_paths: _, - base_container_supports_ingress_host_loopback_allow: _, - isolation_session_available: _, - hyperlight_available: _, - ui_capabilities: _, - } = facts; - assert!(!base_container_api_present); -} - #[cfg(target_os = "macos")] #[test] fn platform_support_macos_is_seatbelt() { diff --git a/tests/scripts/README.md b/tests/scripts/README.md index a5efb1dff..c0e2d04a0 100644 --- a/tests/scripts/README.md +++ b/tests/scripts/README.md @@ -60,11 +60,11 @@ built tree, which is the fastest way to iterate on one area: tests\scripts\run_processcontainer_network_proxy_test.ps1 -RequireTier base-container ``` -When backend discovery reports the `identitylessLoopbackProxy` capability, +When the host probe reports `baseContainerSupportsIdentitylessLoopbackProxy`, the proxy area requires workload launch and a successful proxied fetch on both PSEC 1.0-only hosts and hosts with PSEC 1.1 ingress support. A clean policy rejection is a failure on these hosts. -Older binaries that omit this capability do not enable the capability-specific +Older binaries that omit this probe fact do not enable the capability-specific assertions; the existing proxy assertions still apply. Shared helpers live in `tests/scripts/lib/WinProcessContainer.Common.ps1`. It diff --git a/tests/scripts/lib/WinProcessContainer.Common.ps1 b/tests/scripts/lib/WinProcessContainer.Common.ps1 index e7f9bcc97..28dfab954 100644 --- a/tests/scripts/lib/WinProcessContainer.Common.ps1 +++ b/tests/scripts/lib/WinProcessContainer.Common.ps1 @@ -351,13 +351,9 @@ function Get-HostCapabilities { $enumBit = if ($p.probes.PSObject.Properties['baseContainerSupportsEnumeratePaths']) { [bool]$p.probes.baseContainerSupportsEnumeratePaths } else { $false } - $backends = Invoke-Probe -Wxc $WxcDebug -AvailableBackends -Phase 'P0' -Name 'backend-capabilities' - $processContainer = @($backends | Where-Object { $_ -and $_.backend -eq 'processcontainer' }) - if ($processContainer.Count -ne 1) { - throw 'Get-HostCapabilities: backend discovery did not report exactly one ProcessContainer backend.' - } - $identitylessProxyBit = $processContainer[0].PSObject.Properties['capabilities'] -and - ($processContainer[0].capabilities -contains 'identitylessLoopbackProxy') + $identitylessProxyBit = if ($p.probes.PSObject.Properties['baseContainerSupportsIdentitylessLoopbackProxy']) { + [bool]$p.probes.baseContainerSupportsIdentitylessLoopbackProxy + } else { $false } # uiCapabilities is absent on older binaries / when the detector errored. $canInject = $false if ($p.probes.PSObject.Properties['uiCapabilities'] -and @@ -698,13 +694,12 @@ function New-Config { # Test runners function Invoke-Probe { - param([string]$Wxc, [string]$ConfigPath = $null, [string]$Phase, [string]$Name, - [switch]$AvailableBackends) + param([string]$Wxc, [string]$ConfigPath = $null, [string]$Phase, [string]$Name) # Use ProcessStartInfo so we can keep stdout (the JSON) separate from # stderr (DACL-recovery messages, build-time warnings). $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $Wxc - $argList = @(if ($AvailableBackends) { '--available-backends' } else { '--probe' }) + $argList = @('--probe') if ($ConfigPath) { $argList += @('--config', "`"$ConfigPath`"") } $psi.Arguments = ($argList -join ' ') $psi.RedirectStandardOutput = $true