Skip to content

Report an undetermined WSL version instead of asserting WSL2 - #19158

Merged
Adam Ratzman (adamint) merged 10 commits into
microsoft:mainfrom
adamint:adamint/doctor-unknown-state
Aug 12, 2026
Merged

Report an undetermined WSL version instead of asserting WSL2#19158
Adam Ratzman (adamint) merged 10 commits into
microsoft:mainfrom
adamint:adamint/doctor-unknown-state

Conversation

@adamint

@adamint Adam Ratzman (adamint) commented Aug 7, 2026

Copy link
Copy Markdown
Member

Description

aspire doctor classified WSL by parsing the kernel major version from /proc/version and treating anything >= 4 as WSL2. That premise is wrong: WSL1 reports a synthetic 4.4.0 compatibility banner, so every current WSL1 system was reported as healthy WSL2.

The check now classifies the complete kernel release:

  • 3.4.0-Microsoft and 4.4.0-<build>-Microsoft are WSL1.
  • Microsoft WSL2 release shapes are WSL2.
  • A parseable custom kernel inside an independently established WSL shell is WSL2. Custom kernels are a WSL2-only setting.
  • Missing or malformed releases are Unknown.

WSL_DISTRO_NAME and WSL_INTEROP establish that the shell is in WSL; neither overrides an exact WSL1 banner. Blank values are ignored.

The parser only examines the numeric kernel release token. Build identities such as (Microsoft@builder) do not make native Linux WSL, and malformed tokens such as unknown-microsoft-standard-WSL2 do not produce a false pass.

Unknown produces a warning instead of a false pass or unconditional upgrade instruction.

References:

Verification

Focused WSL tests:
  total: 78
  failed: 0
  succeeded: 78

Aspire.Cli.Tests:
  total: 4895
  failed: 0
  succeeded: 4862
  skipped: 33

The CI-produced linux-arm64 PR artifact 13.6.0-pr.19158.g83a0bb0d was also run through the real aspire doctor path in Ubuntu 24.04. Its embedded SHA matches this PR's 83a0bb0daf head. A private mount namespace supplied each /proc/version fixture. Machine-checked assertions passed for all 22 scenarios, including real WSL1 and WSL2 banners, custom kernels, malformed Microsoft-looking tokens, blank signals, native Linux false positives, and WSL1 precedence.

This proves the shipped classifier and /proc/version read path on Linux. It is not a genuine Windows/WSL-host test.

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No

The WSL check classified the version by parsing a kernel major version out of
/proc/version and treating anything >= 4 as WSL2. WSL1 has no kernel of its own
and always reports a fixed 4.4.0 compatibility banner, so that comparison
classified every real WSL1 system as WSL2. The check then emitted a green
"WSL2 environment detected" row, which meant the limited-container-support
warning the check exists to surface never fired for the users who needed it.

The same method also collapsed two distinct unknowns into confident answers. A
missing, unreadable, or unrecognized /proc/version fell through to WSL2 and
passed, while a Microsoft banner whose version could not be parsed fell through
to WSL1 and told the user to upgrade to a version they may already run.

Classify from the markers each version actually writes: WSL2 kernels carry a
"WSL2" suffix, and WSL1 carries the 4.4.0 "-Microsoft" compatibility banner.
Anything else, including a custom kernel configured through .wslconfig, is now
reported as a distinct warning naming the file that could not be classified,
so doctor never asserts an environment it was unable to observe.

The banner read moves behind a constructor seam so the classification can be
tested without the host being WSL. Reverting the classification fails 7 of the
15 new tests, including a real WSL1 banner reported as WSL2 and an unreadable
banner reported as pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings August 7, 2026 21:02
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 19158

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 19158"

@github-actions github-actions Bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Aug 7, 2026

Copilot AI left a comment

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.

Pull request overview

Corrects aspire doctor WSL detection to avoid misreporting WSL1 or unknown environments as WSL2.

Changes:

  • Adds three-state WSL classification and actionable warnings.
  • Introduces injectable kernel-banner reading.
  • Adds 15 focused regression tests.
Show a summary per file
File Description
src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs Implements marker-based WSL classification.
tests/Aspire.Cli.Tests/Commands/WslEnvironmentCheckTests.cs Covers WSL1, WSL2, unknown, and non-WSL scenarios.

Review details

Suppressed comments (2)

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:159

  • The WSL1 matcher is broader than the fixed marker it is intended to recognize: it accepts every 4.4.x release, while the separate check on line 132 allows Microsoft to occur anywhere in compiler/build metadata. For example, Linux version 4.4.1-custom (Microsoft@builder) is reported as WSL1 instead of unknown. Match the complete 4.4.0-<build>-Microsoft release token so custom/native kernels are not given a confident WSL1 warning.
    [GeneratedRegex(@"Linux\s+version\s+4\.4\.", RegexOptions.IgnoreCase)]

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:121

  • This searches the entire /proc/version payload, not the kernel release token described above. Build metadata follows that token, so a native/custom banner such as Linux version 6.1.0-custom (root@WSL2-builder) ... is classified as WSL2 and produces a passing row even though no WSL2 kernel marker was observed. Restrict the match to the release token after Linux version.
        if (procVersion.Contains("WSL2", StringComparison.OrdinalIgnoreCase))
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs Outdated
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 8, 2026 16:42

Copilot AI left a comment

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.

Review details

Suppressed comments (1)

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:123

  • Microsoft's WSL kernel release notes list the 4.19 WSL2 releases as 4.19.84-microsoft-standard through 4.19.128-microsoft-standard; those banners do not contain the literal WSL2. This condition therefore classifies genuine early WSL2 installations as Unknown and emits an unnecessary warning/upgrade command. Recognize the microsoft-standard marker as WSL2 as well, and cover an actual 4.19 banner in the regression cases.
        // WSL 2 runs a genuine Microsoft-built kernel whose release string carries a "WSL2" marker:
        //   Linux version 5.15.90.1-microsoft-standard-WSL2 (oe-user@oe-host) (...) #1 SMP ...
        if (procVersion.Contains("WSL2", StringComparison.OrdinalIgnoreCase))
        {
            return WslVersion.Wsl2;
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 487b899d-d79d-4349-a84b-a026861bde7f
Copilot AI review requested due to automatic review settings August 9, 2026 20:19

Copilot AI left a comment

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.

Review details

Suppressed comments (2)

src/Aspire.Cli/Utils/EnvironmentChecker/ContainerRuntimeCheck.cs:104

  • Treating a blank primary variable as absent makes doctor disagree with the runtime it diagnoses. DcpOptions reads these keys via GetString(..., fallbackOnEmpty: false) (src/Aspire.Hosting/Dcp/DcpOptions.cs:311), so an empty primary value suppresses the legacy value and defaults DCP to Docker, while whitespace is passed to DCP as the configured runtime (DcpHost.cs:304-307). With the new fallback, this check can instead report the legacy runtime (for example, Podman) as active. Align blank-value handling in the shared/AppHost configuration path and this check so the reported runtime matches what Aspire will launch.
        var configuredRuntime = environment.GetEnvironmentVariable("ASPIRE_CONTAINER_RUNTIME");
        if (!string.IsNullOrWhiteSpace(configuredRuntime))
        {
            return configuredRuntime;
        }

src/Aspire.Cli/Utils/EnvironmentChecker/DevCertsCheck.cs:565

  • This condition routes whitespace to the “unset” branch, but that branch still emits $SSL_CERT_DIR:. With SSL_CERT_DIR=' ', executing the recommendation therefore preserves the whitespace value as the first certificate-directory entry, contrary to the stated goal of treating it as unset. Omit the expansion in the unset branch and update the new test expectation accordingly.
        if (!string.IsNullOrWhiteSpace(currentSslCertDir))
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI left a comment

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.

Review details

Suppressed comments (3)

src/Aspire.Cli/Utils/EnvironmentChecker/ContainerRuntimeCheck.cs:101

  • Treating a blank primary value as absent here makes aspire doctor disagree with the AppHost runtime. DcpOptions resolves these keys with configuration.GetString(...) without fallbackOnEmpty (src/Aspire.Hosting/Dcp/DcpOptions.cs:311), so an empty primary value suppresses the legacy value and whitespace is passed to DCP. In these cases doctor now reports the legacy runtime as configured even though the application will use the default runtime or an invalid whitespace value. Align the shared runtime resolution semantics before applying this fallback in doctor.
        if (!string.IsNullOrWhiteSpace(configuredRuntime))

src/Aspire.Cli/Utils/EnvironmentChecker/DevCertsCheck.cs:565

  • This condition selects the “unset” branch for whitespace, but that branch still emits $SSL_CERT_DIR:. Running the recommendation therefore preserves the whitespace value as a relative certificate directory instead of actually treating it as unset; an empty value also leaves a leading empty path component. Build the unset command solely from the detected system directories and dev-certs path, and update the expectation accordingly.
        if (!string.IsNullOrWhiteSpace(currentSslCertDir))

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:169

  • The WSL1 marker is documented here as the fixed 4.4.0-<build>-Microsoft release, but this regex accepts any Linux version 4.4.* as long as Microsoft appears anywhere else in the banner. For example, Linux version 4.4.1-custom (Microsoft@builder) is confidently reported as WSL1 instead of Unknown, violating the new three-state behavior. Match the complete WSL1 release token rather than the broad version prefix/build metadata combination.
    [GeneratedRegex(@"Linux\s+version\s+4\.4\.", RegexOptions.IgnoreCase)]
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

GetConfiguredRuntime treated a blank ASPIRE_CONTAINER_RUNTIME as absent and fell
back to DOTNET_ASPIRE_CONTAINER_RUNTIME, but DcpOptions resolves the same keys
through GetString(primary, secondary) with fallbackOnEmpty: false, so a present
primary key suppresses the legacy one regardless of its value. Doctor could
therefore name Podman while the AppHost launched the DCP default. Resolve the
same way the AppHost does, and normalize only the empty case, because DcpHost
omits --container-runtime for an empty value but forwards whitespace verbatim.

The SSL_CERT_DIR recommendation for a blank value still expanded $SSL_CERT_DIR,
so running it made the whitespace the first certificate directory OpenSSL
searches - the opposite of treating it as unset - and left a leading empty entry
when the variable was genuinely absent. Build the unset command from the
detected directories alone.

The WSL 1 banner regex matched any 'Linux version 4.4.*' as long as 'Microsoft'
appeared anywhere in the banner, so a custom 4.4.1 kernel built by a user whose
name contains Microsoft was confidently reported as WSL 1 and told to upgrade.
Match the whole fixed 4.4.0-<build>-Microsoft compatibility release instead;
anything else falls through to Unknown, which is the state this check adds.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 487b899d-d79d-4349-a84b-a026861bde7f
Copilot AI review requested due to automatic review settings August 9, 2026 21:14

Copilot AI left a comment

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.

Review details

Suppressed comments (6)

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:100

  • /proc/version includes compiler/build identity metadata, so scanning the whole banner still makes a native or custom kernel such as the test's (Microsoft@builder) example count as WSL. CheckAsync will then emit an incorrect WSL warning. Restrict detection to recognized markers in the kernel release token (or a nonblank WSL environment variable).
        if (procVersion is not null &&
            (procVersion.Contains("microsoft", StringComparison.OrdinalIgnoreCase) ||
             procVersion.Contains("WSL", StringComparison.OrdinalIgnoreCase)))

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:121

  • This searches all banner metadata, so a custom WSL kernel built under an identity such as (wsl2@builder) is reported as a confident WSL2 pass even though its release token has no WSL2 marker. Match only the kernel release token immediately following Linux version, and add an incidental-marker regression case.
        if (procVersion.Contains("WSL2", StringComparison.OrdinalIgnoreCase))

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:149

  • Like the WSL2 check above, this can match compiler/build metadata rather than the release token. A custom kernel banner containing (microsoft-standard@builder) is therefore misclassified as WSL2. Parse the release token and require microsoft-standard there.
        if (procVersion.Contains("microsoft-standard", StringComparison.OrdinalIgnoreCase))

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:87

  • The unknown-version fix still tells every user to “then upgrade,” including WSL2 users whose custom kernel correctly classifies as Unknown. That recreates the misleading guidance this change is intended to remove. Make the upgrade conditional on wsl --list --verbose reporting version 1.

This issue also appears in the following locations of the same file:

  • line 98
  • line 121
  • line 149
            Fix = "Run 'wsl --list --verbose' from Windows to check the version, then upgrade with: wsl --set-version <distro> 2",

src/Aspire.Cli/Utils/EnvironmentChecker/ContainerRuntimeCheck.cs:124

  • Keeping whitespace non-null does not actually surface the invalid configuration. It matches neither probed runtime, so selected remains null and any installed Docker/Podman rows are reported as merely “available” (often with Pass); no result mentions the whitespace value even though AppHost forwards it and fails. Emit an explicit failure for a configured value that matches no supported runtime, and cover the resulting CheckAsync output rather than only this helper.
        var configuredRuntime = environment.GetEnvironmentVariable("ASPIRE_CONTAINER_RUNTIME")
            ?? environment.GetEnvironmentVariable("DOTNET_ASPIRE_CONTAINER_RUNTIME");

        return string.IsNullOrEmpty(configuredRuntime) ? null : configuredRuntime;

src/Aspire.Cli/Utils/EnvironmentChecker/ContainerRuntimeCheck.cs:124

  • The PR description says a blank ASPIRE_CONTAINER_RUNTIME falls back to DOTNET_ASPIRE_CONTAINER_RUNTIME, but ?? runs before empty normalization, so an empty primary suppresses the legacy value; the new tests explicitly expect null. Either update the PR description to document this AppHost-compatible behavior or change both AppHost and doctor to use empty-value fallback.
        var configuredRuntime = environment.GetEnvironmentVariable("ASPIRE_CONTAINER_RUNTIME")
            ?? environment.GetEnvironmentVariable("DOTNET_ASPIRE_CONTAINER_RUNTIME");

        return string.IsNullOrEmpty(configuredRuntime) ? null : configuredRuntime;
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Copilot AI left a comment

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.

Review details

Suppressed comments (1)

src/Aspire.Cli/Utils/EnvironmentChecker/ContainerRuntimeCheck.cs:153

  • This contradicts the PR description, which says a blank ASPIRE_CONTAINER_RUNTIME is treated as unset so DOTNET_ASPIRE_CONTAINER_RUNTIME can be used. Here an empty primary value suppresses the fallback, while whitespace remains configured; the new tests explicitly lock in that behavior. Please align the description and intended behavior (the current implementation mirrors DcpOptions at src/Aspire.Hosting/Dcp/DcpOptions.cs:311).
        var configuredRuntime = environment.GetEnvironmentVariable("ASPIRE_CONTAINER_RUNTIME")
            ?? environment.GetEnvironmentVariable("DOTNET_ASPIRE_CONTAINER_RUNTIME");

        return string.IsNullOrEmpty(configuredRuntime) ? null : configuredRuntime;
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 0d1cf760-b1bc-46ba-a6d4-628354b00f2c
Copilot AI review requested due to automatic review settings August 10, 2026 15:01

Copilot AI left a comment

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.

Review details

Suppressed comments (2)

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:157

  • This broad substring match has the same false-pass behavior for the early marker: a custom release such as 6.1.0-microsoft-standard-custom is classified as WSL2. The documented early WSL2 releases end in -microsoft-standard; require that suffix so custom or unrecognized releases remain Unknown.
        if (kernelRelease.Contains("microsoft-standard", StringComparison.OrdinalIgnoreCase))

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:130

  • This accepts any release token containing WSL2, so a custom/native release such as 6.1.0-custom-WSL2 is reported as a healthy WSL2 environment. Official kernels use the -microsoft-standard-WSL2 local-version suffix; require that suffix so unrecognized releases fall through to Unknown, and add this case to the regression matrix.

This issue also appears on line 157 of the same file.

        if (kernelRelease.Contains("WSL2", StringComparison.OrdinalIgnoreCase))
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3253974d-4f18-486f-863c-281a607656d4
Copilot AI review requested due to automatic review settings August 11, 2026 04:54

Copilot AI left a comment

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.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@github-actions

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@mitchdenny Mitch Denny (mitchdenny) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the WSL detection changes and regression coverage; no blocking issues found.

@adamint
Adam Ratzman (adamint) marked this pull request as draft August 12, 2026 01:34
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 461fd6a2-28f3-44e1-a316-14d594b50226
Copilot AI review requested due to automatic review settings August 12, 2026 09:35

Copilot AI left a comment

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.

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings August 12, 2026 16:28

Copilot AI left a comment

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.

Review details

Suppressed comments (1)

src/Aspire.Cli/Utils/EnvironmentChecker/WslEnvironmentCheck.cs:264

  • NumericKernelRelease rejects a release ending in + because \S+ requires at least one character after the suffix delimiter. This makes the newly added CustomTerminalPlusKernelBanner cases return Unknown instead of the asserted WSL2 pass. Allow + to be terminal while retaining the existing validation for other suffixes.
    [GeneratedRegex(@"^\d+(?:\.\d+)+(?:[-+._~]\S+)?$", RegexOptions.CultureInvariant)]
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@adamint
Adam Ratzman (adamint) marked this pull request as ready for review August 12, 2026 17:02
@adamint
Adam Ratzman (adamint) enabled auto-merge (squash) August 12, 2026 17:02
@adamint
Adam Ratzman (adamint) merged commit c94dbb8 into microsoft:main Aug 12, 2026
719 of 722 checks passed
@github-actions github-actions Bot added this to the 13.6 milestone Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants