Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions docs/backends/wslc/wsl-container-getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ MXC, which lets you run Linux containers on Windows using the WSLC SDK.
| Requirement | Details |
|---|---|
| **Windows 10 1903 (build 18362.1049)+ on x64, or Windows 10 2004 (build 19041)+ on ARM64** | WSL 2's own [system requirements](https://learn.microsoft.com/windows/wsl/install-manual#step-2---check-requirements-for-running-wsl-2). MXC does not check the OS version itself β€” see [How an unsupported Windows version is reported](#how-an-unsupported-windows-version-is-reported) below. The WSL runtime package installs only on build 19041 and later, so in practice a 1903/1909 host still needs an upgrade to reach the WSL version below. |
| **WSL 3.0.3+** | The installed WSL runtime package must meet the WSLC minimum; see Step 1 below for installation |
| **WSL 3.0.5+** | The installed WSL runtime package must meet the WSLC minimum; see Step 1 below for installation |
| **WSLC SDK** | `wslcsdk.dll` is a separate client SDK and must be in the same directory as the running executable (`wxc-exec.exe`, or your own binary when using the Rust SDK) |
| **Container images** | Reachable from a registry, already cached, or supplied as a local tar |

Expand Down Expand Up @@ -54,9 +54,9 @@ optional component is absent as well. Any SDK call that reaches the WSL service
can return it, so an unexpected `0x80040327` from a WSLC operation means the host
OS is below WSL 2's floor, not that WSL is merely uninstalled.

## Step 1 β€” Install WSL 3.0.3+
## Step 1 β€” Install WSL 3.0.5+

The WSLC SDK requires WSL version 3.0.3 or later. Update WSL to the latest
The WSLC SDK requires WSL version 3.0.5 or later. Update WSL to the latest
version:

```powershell
Expand All @@ -69,7 +69,7 @@ Verify your WSL version after updating:
wsl --version
```

The WSL version should be **3.0.3.0 or later**. If `wsl --update`
The WSL version should be **3.0.5.0 or later**. If `wsl --update`
does not bring you to the required version, build WSL from the `master`
branch:

Expand Down Expand Up @@ -518,7 +518,7 @@ no separate `--setup-wslc` step is required.

| Exact v0.9 policy | WSLC behavior |
|---|---|
| Egress, ingress, and host-loopback defaults all `allow` | Bridged networking without independent directional filtering |
| Egress, ingress, and host-loopback defaults all `allow` | Bridged networking without independent directional filtering; reaches [host loopback](#host-loopback) |
| All three defaults `deny` (also the omitted defaults) | No networking (isolated) |
| Mixed allow/deny directions or egress rules | Rejected; WSLC cannot enforce that combination |

Expand All @@ -527,6 +527,22 @@ no separate `--setup-wslc` step is required.
> unenforced. The removed legacy `allowOutbound` setting and JSON host-list
> fields are not accepted by any supported contract.

### Host loopback

MXC enables WSL host loopback on every session it creates. A bridged container reaches a service bound to Windows `127.0.0.1`
through the hostname `host.wslc.internal`:

```sh
wget -qO- http://host.wslc.internal:8080/
```

- Every Windows loopback TCP port is reachable; there is no per-port filter.

- **Behavior change:** existing all-`allow` configs gain outbound access to
Windows loopback services.

- Inbound connections from Windows still need declared port mappings.

### Network proxy (cooperative, unprivileged)

WSLC supports a **cooperative HTTP/HTTPS proxy**: setting `runtimeConfig.networkProxy`
Expand All @@ -553,8 +569,9 @@ variables that well-behaved clients honor.

**The runtime field contains a URL string.** A WSLC container runs in its own
network namespace (a separate WSL system VM). Its own `127.0.0.1` loopback is
valid for a proxy running inside that container, as in the test fixture below;
a proxy on the host's or WSL distro's loopback is **not reachable**. An external
valid for a proxy running inside that container, as in the test fixture below.
The host's or WSL distro's `127.0.0.1` is **not reachable** at that address; a
proxy on Windows loopback is reachable through `host.wslc.internal` (see [Host loopback](#host-loopback)). An external
proxy must have an address routable from the container:

```json
Expand Down Expand Up @@ -763,7 +780,7 @@ images β€” cannot be used.
| `WSLC runtime unavailable` **on a host below WSL 2's minimum Windows version** | The WSL runtime package cannot install on this OS, so it reports as missing β€” MXC does not distinguish the two cases | Run `wsl --update`; WSL reports the version verdict ("Windows version {} does not support the packaged version of Windows Subsystem for Linux"). Upgrade Windows β€” see [How an unsupported Windows version is reported](#how-an-unsupported-windows-version-is-reported) |
| HRESULT `0x80040327` (`WSL_E_OS_NOT_SUPPORTED`) from any WSLC call | The SDK reached WSL's service-connect guard on a host that is neither Windows 11 nor has the WSL support interface | Upgrade Windows. `0x80040321` (`WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED`) is the sibling code when the legacy `lxss` component is absent too |
| `Failed to load wslcsdk.dll` | DLL not in same directory as `wxc-exec.exe` | Copy `wslcsdk.dll` next to the binary |
| `WSLC runtime unavailable` | WSL runtime package is missing, older than 3.0.3, or the Virtual Machine Platform optional component is disabled | Update WSL with `wsl --update`, verify the installed version with `wsl --version`, and enable the Virtual Machine Platform optional component if required. The WSLC SDK DLL is a separate dependency and does not replace the WSL runtime package. |
| `WSLC runtime unavailable` | WSL runtime package is missing, older than 3.0.5, or the Virtual Machine Platform optional component is disabled | Update WSL with `wsl --update`, verify the installed version with `wsl --version`, and enable the Virtual Machine Platform optional component if required. The WSLC SDK DLL is a separate dependency and does not replace the WSL runtime package. |
| `WSLC runtime unavailable. Missing components: SdkNeedsUpdate` | The opposite direction: your installed WSL is **newer** than the WSLc SDK this MXC build ships (specified by `WSLC_SDK_VERSION` in `src/mxc-sdk/build/build_wslc_common.rs`) | Update MXC to a build that includes a newer SDK. Do **not** update WSL β€” it is already ahead, and updating it further will not clear this. |
| `WSLC image '<name>' is not cached, and this sandbox declares no egress` | An isolated config named an image the store does not have | Warm the cache with `--setup-wslc`, set `imageTarPath`, or allow egress. For a lifecycle operation the store is a daemon slot, so warm it with `setup-wslc.ps1 -DaemonSlots 1` |
| `WSLc store unavailable: the image store at <path> is already held by another live session` | Two processes reached the same store, which admits one live session | Wait for the holder to release it. A running `wxc-wslc-daemon.exe` releases its slot after `MXC_WSLC_DAEMON_IDLE_TIMEOUT_SECS` (default 300s); a create-and-run execution releases its store when the run ends |
Expand All @@ -779,6 +796,7 @@ images β€” cannot be used.

- [`tests/examples/wslc_hello_world.json`](../../../tests/examples/wslc_hello_world.json) β€” Hello world with Alpine
- [`tests/configs/wslc_network_isolated.json`](../../../tests/configs/wslc_network_isolated.json) β€” Network isolation
- [`tests/configs/wslc_network_host_loopback.json`](../../../tests/configs/wslc_network_host_loopback.json) β€” Bridged container reaches a Windows `127.0.0.1` service through `host.wslc.internal` (driven by `run_wslc_host_loopback_test.ps1`)
- [`tests/configs/wslc_network_proxy.json`](../../../tests/configs/wslc_network_proxy.json) β€” In-container cooperative HTTP proxy (`runtimeConfig.networkProxy` at the container's `127.0.0.1`)
- [`tests/configs/wslc_custom_registry_ghcr.json`](../../../tests/configs/wslc_custom_registry_ghcr.json) β€” Pull from GitHub Container Registry
- [`tests/configs/wslc_custom_registry_quay.json`](../../../tests/configs/wslc_custom_registry_quay.json) β€” Pull from Quay.io
Expand Down
6 changes: 4 additions & 2 deletions docs/backends/wslc/wslc-state-aware.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Native builds still require the `wslc` feature (`build.bat --with-wslc`).

## Why a daemon

The WSLc SDK (`wslcsdk.dll`, 3.0.3) has **no cross-process session re-attach**: every operation
The WSLc SDK (`wslcsdk.dll`, 3.0.5) has **no cross-process session re-attach**: every operation
(`WslcCreateContainer`, `WslcStartContainer`, `WslcCreateContainerProcess`, image pull, stop /
delete) requires a live in-process `WslcSession` / `WslcContainer` handle, and there is no
`WslcOpenSession`. Each lifecycle phase runs as a **separate**
Expand Down Expand Up @@ -640,7 +640,9 @@ Omitted directional values default to deny, so an egress-only allow request is
rejected rather than claiming its implicit ingress/loopback denies are enforced.
Mixed postures and per-host rules are rejected. Allowing ingress/host-loopback
does not create host port forwarding or promise reachability across NAT; it
acknowledges that WSLC cannot independently restrict those directions.
acknowledges that WSLC cannot independently restrict those directions. The
daemon's sessions enable host loopback, so bridged containers reach Windows `127.0.0.1` services through `host.wslc.internal`; see
[Host loopback](wsl-container-getting-started.md#host-loopback).

| Field | provision | start / stop / deprovision | exec |
|-------|-----------|----------------------------|------|
Expand Down
2 changes: 2 additions & 0 deletions docs/development/build-and-test/wslc-sdk-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ match the pinned SDK.
cargo test -p mxc-sdk --features wslc --lib wslc::common # includes ABI size/enum asserts
cargo fmt --all -- --check
cargo clippy -p mxc-sdk --features wslc --all-targets -- -D warnings
cd ..
.\scripts\generate-wslc-bindings.ps1 -Check
```

7. **Run the WSLC E2E suite on a live WSL host** (cannot run in GitHub Actions β€”
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-wslc-bindings.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ try {
# newtypes via --bitfield-enum; exclusive-value enums become opaque
# newtypes via --newtype-enum. Both keep the C ABI while avoiding UB from
# out-of-range discriminants that a plain Rust `enum` would allow.
$bitfieldEnums = 'Wslc(ContainerFlags|SessionFeatureFlags|ContainerStartFlags|DeleteContainerFlags|ComponentFlags|VhdRequirementsFlags|ProcessFlags)'
$bitfieldEnums = 'Wslc(ContainerFlags|SessionFeatureFlags|ContainerStartFlags|DeleteContainerFlags|ComponentFlags|VhdRequirementsFlags|ProcessFlags|NetworkFlags)'
$newtypeEnums = 'Wslc(PortProtocol|Signal|ProcessIOHandle|ContainerNetworkingMode|VhdType|SessionTerminationReason|ContainerState|ProcessState|ImageProgressStatus)'

$generatedFile = if ($Check) { Join-Path $staging "wslcsdk_sys.rs" } else { $outFile }
Expand Down
5 changes: 4 additions & 1 deletion src/mxc-sdk/build/build_wslc_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;

/// Pinned WSLC SDK version. Override with the `WSLC_SDK_VERSION` env var.
const WSLC_SDK_VERSION: &str = "3.0.3";
const WSLC_SDK_VERSION: &str = "3.0.5";

/// Direct anonymous content URL for a package version on the public
/// MxcDependencies Azure Artifacts feed (mirrors nuget.org; reachable from the
Expand Down Expand Up @@ -293,6 +293,9 @@ fn expected_sha256(version: &str) -> Option<String> {
// version. This is an exact-bytes content pin, independent of the NuGet
// author signature.
match version {
"3.0.5" => {
Some("23e905e0f0925834d9960d4d752f8bedcbd34cb59a20fb0578c6336745f8e88f".to_string())
}
"3.0.3" => {
Some("d31f78cc4816ccccc0dc196ecf4d2b27911b8ed677b4b9cc697274e941b6511b".to_string())
}
Expand Down
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions src/mxc-sdk/build/wslc_common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**Package:** `Microsoft.WSL.Containers`
**Version:** pinned in `src/mxc-sdk/build/build_wslc_common.rs`
(`WSLC_SDK_VERSION`, currently **3.0.3**)
(`WSLC_SDK_VERSION`, currently **3.0.5**)
**Source:** the [`MxcDependencies`](https://dev.azure.com/shine-oss/mxc/_artifacts/feed/MxcDependencies)
Azure Artifacts feed β€” a public, anonymous-read mirror of nuget.org.

Expand Down Expand Up @@ -42,7 +42,7 @@ environment variable.

## Contents

- `Microsoft.WSL.Containers.3.0.3.nupkg` β€” vendored fallback copy of the SDK
- `Microsoft.WSL.Containers.<version>.nupkg` β€” vendored fallback copy of the SDK
(header `wslcsdk.h`, import libs `wslcsdk.lib`, and runtime `wslcsdk.dll` for
x64 and ARM64). Must match `WSLC_SDK_VERSION` in `build.rs`.

Expand Down
49 changes: 41 additions & 8 deletions src/mxc-sdk/src/backends/wslc/common/container_steps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
//! move that happens *before* any `&raw` is taken), after which callers keep it
//! as a stationary local.

use std::ffi::c_void;
use std::ffi::{c_void, CStr};
use std::fmt::Write;
use std::ptr;
use std::sync::atomic::{AtomicBool, Ordering};
Expand Down Expand Up @@ -845,15 +845,9 @@ pub unsafe fn create_daemon_session(
}
apply_session_sizing(sdk, &mut settings, sizing)?;

let mut session: WslcSession = ptr::null_mut();
let mut err_msg = CoTaskMemPWSTR::null();
let started = Instant::now();
let hr = sdk.WslcCreateSession(&mut settings, &mut session, err_msg.as_mut_ptr());
let session = create_session(sdk, &mut settings)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The daemon now enables host loopback, but PROTOCOL_VERSION stays 9 and ready_daemon() reuses any ready v9 daemon, so after an upgrade a still-running pre-PR daemon (alive while it holds sandboxes) keeps creating sessions without host.wslc.internal, and a new daemon gives old clients the new access.

Shall we bump the protocol version?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will bump.

let created_in = started.elapsed();
if hr != S_OK {
let msg = err_msg.to_string_lossy();
return Err(sdk_error("WslcCreateSession failed", hr, &msg).into());
}

let guard = WslcSessionGuard::from_raw(
session,
Expand Down Expand Up @@ -898,6 +892,45 @@ pub unsafe fn create_daemon_session(
Ok(guard)
}

/// Hostname that resolves to the Windows host loopback inside a session. WSL's
/// own CLI uses the same default.
pub(crate) const HOST_LOOPBACK_NAME: &CStr = c"host.wslc.internal";

/// Create a session with host loopback enabled. A WSL runtime older than 3.0.5
/// rejects the setting.
Comment on lines +899 to +900

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isolated-only sessions (every deny-all create-and-run, where network_is_isolated(request) is already known) gain nothing from host loopback, yet setting it forces WSL's new session path, so they now fail below WSL 3.0.5 while available_backends() still reports WSLC available; #1625 README.md:80-81 and 407-410 say MXC sets it only on 3.0.5+ and NONE/BRIDGED keep working on older runtimes.

Set the hostname only for sessions that can hold a bridged container (pass !network_is_isolated(request) from the runner), or update #1625 and make discovery report the 3.0.5 requirement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.0.5 is now the minimum version. I will see if I can enforce that.

///
/// # Safety
/// `sdk` must hold valid function pointers, `settings` must be initialised and
/// COM must be initialised on this thread.
pub(crate) unsafe fn create_session(
sdk: &WslcSdk,
settings: &mut WslcSessionSettings,
) -> Result<WslcSession, ScriptResponse> {
let hr = sdk.WslcSetSessionSettingsHostLoopback(settings, HOST_LOOPBACK_NAME.as_ptr());
if hr != S_OK {
return Err(sdk_error(
"WslcSetSessionSettingsHostLoopback failed",
hr,
"",
));
}

let mut session: WslcSession = ptr::null_mut();
let mut err_msg = CoTaskMemPWSTR::null();
let hr = sdk.WslcCreateSession(settings, &mut session, err_msg.as_mut_ptr());
if hr == WSLC_E_RUNTIME_UPDATE_NEEDED {
Comment thread
theelliotm marked this conversation as resolved.
Resolved
return Err(WslcError::Unavailable(sdk_init::prerequisite_error(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the daemon this Unavailable surfaces on the boot thread, and finish_session rewraps it as WorkerError::Backend for every provision parked on the implicit session, so a lifecycle provision on WSL < 3.0.5 gets backend_error instead of the backend_unavailable documented at wslc-state-aware.md:701.
Forward Unavailable/Rejected unchanged to parked replies and add a session_manager test for a boot that fails with Unavailable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3.0.5 is now the minimum version. I will see if I can enforce that.

WslcComponentFlags::WSLC_COMPONENT_FLAG_WSL_PACKAGE,
))
.into_response());
}
if hr != S_OK {
let msg = err_msg.to_string_lossy();
return Err(sdk_error("WslcCreateSession failed", hr, &msg));
}
Ok(session)
}

/// Apply a session's sizing and its boot deadline to `settings`.
///
/// The boot timeout is applied even when the caller asked for no sizing, since
Expand Down
2 changes: 1 addition & 1 deletion src/mxc-sdk/src/backends/wslc/common/sdk_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub(crate) fn prerequisite_error(missing: WslcComponentFlags) -> String {
);
}
if needs_wsl_package {
guidance.push("install WSL 3.0.3 or newer by running `wsl --update`".to_string());
guidance.push("install WSL 3.0.5 or newer by running `wsl --update`".to_string());
}
if needs_sdk_update {
// MXC's vendored SDK is behind the installed WSL, so the fix is on MXC's
Expand Down
11 changes: 2 additions & 9 deletions src/mxc-sdk/src/backends/wslc/common/wsl_container_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,14 +659,7 @@ impl WSLContainerRunner {
}
}

// Create session while string data is still alive
let mut session: WslcSession = ptr::null_mut();
let mut err_msg = CoTaskMemPWSTR::null();
let hr = sdk.WslcCreateSession(&mut settings, &mut session, err_msg.as_mut_ptr());
if hr != S_OK {
let msg = err_msg.to_string_lossy();
return Err(sdk_error("WslcCreateSession failed", hr, &msg));
}
let session = container_steps::create_session(sdk, &mut settings)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On x64/WSL 3.0.5 a bridged create-and-run sandbox fetched another sandbox's port mapping via host.wslc.internal:<windowsPort>: MXC passes a null windowsAddress, so WSL publishes every mapping on Windows 127.0.0.1, and this relay opens all of them to every bridged sandbox.

Don't enable session host loopback until per-container/per-port enforcement exists, and add a two-sandbox negative test to run_wslc_host_loopback_test.ps1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow. Investigating.

let _ = writeln!(logger, "[WSLC] Session created: {session_name_str}");

Ok(WslcSessionGuard::from_raw(
Expand Down Expand Up @@ -3274,7 +3267,7 @@ mod tests {
sdk_init::prerequisite_error(WslcComponentFlags::WSLC_COMPONENT_FLAG_WSL_PACKAGE);

assert!(message.contains("WslPackage"));
assert!(message.contains("3.0.3"));
assert!(message.contains("3.0.5"));
assert!(message.contains("wsl --update"));
assert!(!message.contains("--pre-release"));
assert!(!message.contains("Virtual Machine Platform"));
Expand Down
4 changes: 4 additions & 0 deletions src/mxc-sdk/src/backends/wslc/common/wslc_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ pub const WSLC_E_IMAGE_NOT_FOUND: HRESULT = wslc_error(1);
/// Administrative policy on this host forbids reaching the registry.
pub const WSLC_E_REGISTRY_BLOCKED_BY_POLICY: HRESULT = wslc_error(13);

/// The installed WSL runtime is older than a requested setting needs.
pub const WSLC_E_RUNTIME_UPDATE_NEEDED: HRESULT = wslc_error(21);

// ---------------------------------------------------------------------------
// Raw function-pointer types used by the RAII guards.
//
Expand Down Expand Up @@ -156,6 +159,7 @@ impl WslcSdk {
WslcSetSessionSettingsMemory,
WslcSetSessionSettingsTimeout,
WslcSetSessionSettingsFeatureFlags,
WslcSetSessionSettingsHostLoopback,
WslcTerminateSession,
WslcReleaseSession,
WslcInitContainerSettings,
Expand Down
Loading
Loading