From 3167b72018cadba182ac0e0a8ca973c7b2f7cdaf Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 10 Mar 2026 12:34:56 -0700 Subject: [PATCH 01/23] test: enable virtiofs tests and enable WSLG during testing (#14387) * test: enable virtiofs tests and enable WSLG during testing * test fix --------- Co-authored-by: Ben Hillis --- test/windows/Common.cpp | 4 +--- test/windows/DrvFsTests.cpp | 6 ++---- test/windows/UnitTests.cpp | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index cee8848e4..de742a08f 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -1450,8 +1450,6 @@ std::wstring LxssGenerateTestConfig(TestConfigDefaults Default) return value; }; - // TODO: Reset guiApplications to true by default once the virtio hang is solved. - std::wstring newConfig = L"[wsl2]\n" L"crashDumpFolder=" + @@ -1464,7 +1462,7 @@ std::wstring LxssGenerateTestConfig(TestConfigDefaults Default) EscapePath(kernelLogs) + L"\n" L"telemetry=false\n" + - boolOptionToString(L"safeMode", Default.safeMode, false) + boolOptionToString(L"guiApplications", Default.guiApplications, false) + + boolOptionToString(L"safeMode", Default.safeMode, false) + boolOptionToString(L"guiApplications", Default.guiApplications, true) + L"earlyBootLogging=false\n" + networkingModeToString(Default.networkingMode) + drvFsModeToString(Default.drvFsMode); if (Default.kernel.has_value()) diff --git a/test/windows/DrvFsTests.cpp b/test/windows/DrvFsTests.cpp index 7e012b9c8..9307b6961 100644 --- a/test/windows/DrvFsTests.cpp +++ b/test/windows/DrvFsTests.cpp @@ -1341,12 +1341,10 @@ class WSL1 : public DrvFsTests WSL2_DRVFS_TEST_CLASS(Plan9); +WSL2_DRVFS_TEST_CLASS(VirtioFs); + // Disabled while an issue with the 6.1 Linux kernel causing disk corruption is investigated. // TODO: Enable again once the issue is resolved // WSL2_DRVFS_TEST_CLASS(Virtio9p); -// Disabled because it causes too much noise. -// TODO: Enable again once virtiofs is stable -// WSL2_DRVFS_TEST_CLASS(VirtioFs); - } // namespace DrvFsTests \ No newline at end of file diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 5dfbb4ddf..8d5e6b39f 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -271,6 +271,7 @@ class UnitTests }; // Validate user sessions state with gui apps disabled. + WslConfigChange config(LxssGenerateTestConfig({.guiApplications = false})); { validateUserSession(); @@ -284,7 +285,7 @@ class UnitTests // Validate user sessions state with gui apps enabled. { - WslConfigChange config(LxssGenerateTestConfig({.guiApplications = true})); + config.Update(LxssGenerateTestConfig({.guiApplications = true})); validateUserSession(); auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"--user {} echo $DISPLAY", LXSST_TEST_USERNAME)); From 2e75df324777dcde3a7628f4e69a44600b924ae2 Mon Sep 17 00:00:00 2001 From: AlmaLinux Autobot <107999298+almalinuxautobot@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:05:42 +0100 Subject: [PATCH 02/23] chore(distributions): Almalinux auto-update - 20260311 14:52:02 (#14404) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- distributions/DistributionInfo.json | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/distributions/DistributionInfo.json b/distributions/DistributionInfo.json index 52e307ff5..c8e63a679 100644 --- a/distributions/DistributionInfo.json +++ b/distributions/DistributionInfo.json @@ -112,12 +112,12 @@ "FriendlyName": "AlmaLinux OS 8", "Default": false, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20250415.0/AlmaLinux-8.10_x64_20250415.0.wsl", - "Sha256": "34c3bc6d3ac693968737c65db52b67f68b8c1a6f8b024450819841a967f59a3d" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20260311.0/AlmaLinux-8.10_x64_20260311.0.wsl", + "Sha256": "77a662e2947a1482087e3edf22595d62121ad8011385152f9209734adac87941" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20250415.0/AlmaLinux-8.10_ARM64_20250415.0.wsl", - "Sha256": "bd34f64b4822f6f115058f79cdbba85a1560360efbec14c3d699695023f8ca19" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v8.10.20260311.0/AlmaLinux-8.10_ARM64_20260311.0.wsl", + "Sha256": "7fb2e5d25cbba9bfeef1f9e1420fa4e30b671264aa92a015adf8f2901cdfb97f" } }, { @@ -125,12 +125,12 @@ "FriendlyName": "AlmaLinux OS 9", "Default": false, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20251119.0/AlmaLinux-9.7_x64_20251119.0.wsl", - "Sha256": "0a6588f4f723fcb3edbc37dd3e3e13be8ffe0a5027e47513e3d4d2a4451794e7" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20260311.0/AlmaLinux-9.7_x64_20260311.0.wsl", + "Sha256": "c7d9c8f9bbe8f52a34d920988d6cea913f0251c457aef55f5ed66cce90561f92" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20251119.0/AlmaLinux-9.7_ARM64_20251119.0.wsl", - "Sha256": "b3e2632efe029a81db1ae7a914ea3b7e9e6e20d8c71c158270c331e6fea39bf8" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v9.7.20260311.0/AlmaLinux-9.7_ARM64_20260311.0.wsl", + "Sha256": "e9b5e184aa04e6f72574e2c29cda919d4aaed0c7408dcb81905f6c9391516c7f" } }, { @@ -138,12 +138,12 @@ "FriendlyName": "AlmaLinux OS Kitten 10", "Default": false, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20251030.0/AlmaLinux-Kitten-10_x64_20251030.0.wsl", - "Sha256": "d765d65076b041f3a67ba60edc37d056eeab2a260aed8e077684e05b78ecd9f5" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20260311.0/AlmaLinux-Kitten-10_x64_20260311.0.wsl", + "Sha256": "b04ab8ae277ca4bbeb70a15381124609ca7f97b8f7c275930b3fd91195c385ad" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20251030.0/AlmaLinux-Kitten-10_ARM64_20251030.0.wsl", - "Sha256": "90c30b0adbf8d414c4b0a02eaeb6a5d8e488a2187a67dbaf11f4a3e843baae53" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10-kitten.20260311.0/AlmaLinux-Kitten-10_ARM64_20260311.0.wsl", + "Sha256": "d502ab27654fa326888940ebce573dbe5abf7e44f709e67b53030da49572a92f" } }, { @@ -151,12 +151,12 @@ "FriendlyName": "AlmaLinux OS 10", "Default": true, "Amd64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20251124.0/AlmaLinux-10.1_x64_20251124.0.wsl", - "Sha256": "24e8fa286a4081979d97e83a227fb89f332bcf731fe4b422679a3b455ab0be37" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20260311.0/AlmaLinux-10.1_x64_20260311.0.wsl", + "Sha256": "6bc470081100ec507d933ee58b467f1903acc07da719ad37532b942439fc16c4" }, "Arm64Url": { - "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20251124.0/AlmaLinux-10.1_ARM64_20251124.0.wsl", - "Sha256": "20700a4467214074f8a1a3d4e0e1cad25af36b8127d047ab6d5b4a1355e998b8" + "Url": "https://github.com/AlmaLinux/wsl-images/releases/download/v10.1.20260311.0/AlmaLinux-10.1_ARM64_20260311.0.wsl", + "Sha256": "2aedea16c5bdbc83b61d78634cc2f6b839435ad9066df1ea1a0bba7ad2fbdc39" } } ], From 515e7900022c8ccd7b379f30e015e5f92596bd96 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 11 Mar 2026 16:39:21 -0700 Subject: [PATCH 03/23] Fix CVE-2026-26127: bump .NET runtime from 10.0.0 to 10.0.4 (#14421) Addresses Dependabot alerts #10 and #11. The Microsoft.NETCore.App.Runtime packages (win-x64 and win-arm64) at version 10.0.0 are vulnerable to a denial of service via out-of-bounds read when decoding malformed Base64Url input (CVSS 7.5 High). Bumped to 10.0.4 which includes the fix. Co-authored-by: Ben Hillis --- packages.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages.config b/packages.config index e52188f5c..a0f2af944 100644 --- a/packages.config +++ b/packages.config @@ -8,8 +8,8 @@ - - + + From 24c3a9adf7272086e298aede2340ef29bdd70592 Mon Sep 17 00:00:00 2001 From: Blue Date: Thu, 12 Mar 2026 06:11:29 +0000 Subject: [PATCH 04/23] Notice change from build: 141806547 (#14423) Co-authored-by: WSL notice --- NOTICE.txt | 186 ----------------------------------------------------- 1 file changed, 186 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index 02353a23b..24abab47e 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -636,192 +636,6 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---------------------------------------------------------- - ---------------------------------------------------------- - -Microsoft.NETCore.App.Runtime.win-arm64 10.0.0 - MIT - - -Copyright (c) 2021 -Copyright (c) Six Labors -(c) Microsoft Corporation -Copyright (c) 2022 FormatJS -Copyright (c) Andrew Arnott -Copyright 2019 LLVM Project -Copyright (c) 1998 Microsoft -Copyright 2018 Daniel Lemire -Copyright (c) .NET Foundation -Copyright 1995-2022 Mark Adler -Copyright 1995-2024 Mark Adler -Copyright (c) 2011, Google Inc. -Copyright (c) 2020 Dan Shechter -(c) 1997-2005 Sean Eron Anderson -Copyright (c) 2015 Andrew Gallant -Copyright (c) 2022, Wojciech Mula -Copyright (c) 2017 Yoshifumi Kawai -Copyright (c) 2022, Geoff Langdale -Copyright (c) 2005-2020 Rich Felker -Copyright (c) 2012-2021 Yann Collet -Copyright (c) Microsoft Corporation -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2024 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2018 Nemanja Mijailovic -Copyright 2012 the V8 project authors -Copyright (c) 1999 Lucent Technologies -Copyright (c) 2008-2016, Wojciech Mula -Copyright (c) 2011-2020 Microsoft Corp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2015-2018, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Copyright (c) 2015 The Chromium Authors -Copyright (c) 2018 Alexander Chermyanin -Copyright (c) The Internet Society 1997 -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2011-2015 Intel Corporation -Copyright (c) 2013-2017, Milosz Krajewski -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) The Internet Society (2003) -Copyright (c) .NET Foundation Contributors -(c) 1995-2024 Jean-loup Gailly and Mark Adler -Copyright (c) 2020 Mara Bos -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2012 - present, Victor Zverovich -Copyright (c) 2006 Jb Evain (jbevain@gmail.com) -Copyright (c) 2008-2020 Advanced Micro Devices, Inc. -Copyright (c) 2019 Microsoft Corporation, Daan Leijen -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors -Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com -Copyright 1995-2024 Jean-loup Gailly and Mark Adler Qkkbal -Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Portions (c) International Organization for Standardization 1986 -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang) Disclaimers -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip -Copyright (c) 1980, 1986, 1993 The Regents of the University of California -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ---------------------------------------------------------- - ---------------------------------------------------------- - -Microsoft.NETCore.App.Runtime.win-x64 10.0.0 - MIT - - -Copyright (c) 2021 -Copyright (c) Six Labors -(c) Microsoft Corporation -Copyright (c) 2022 FormatJS -Copyright (c) Andrew Arnott -Copyright 2019 LLVM Project -Copyright (c) 1998 Microsoft -Copyright 2018 Daniel Lemire -Copyright (c) .NET Foundation -Copyright 1995-2022 Mark Adler -Copyright 1995-2024 Mark Adler -Copyright (c) 2011, Google Inc. -Copyright (c) 2020 Dan Shechter -(c) 1997-2005 Sean Eron Anderson -Copyright (c) 2015 Andrew Gallant -Copyright (c) 2022, Wojciech Mula -Copyright (c) 2017 Yoshifumi Kawai -Copyright (c) 2022, Geoff Langdale -Copyright (c) 2005-2020 Rich Felker -Copyright (c) 2012-2021 Yann Collet -Copyright (c) Microsoft Corporation -Copyright (c) 2007 James Newton-King -Copyright (c) 1991-2024 Unicode, Inc. -Copyright (c) 2013-2017, Alfred Klomp -Copyright (c) 2018 Nemanja Mijailovic -Copyright 2012 the V8 project authors -Copyright (c) 1999 Lucent Technologies -Copyright (c) 2008-2016, Wojciech Mula -Copyright (c) 2011-2020 Microsoft Corp -Copyright (c) 2015-2017, Wojciech Mula -Copyright (c) 2015-2018, Wojciech Mula -Copyright (c) 2005-2007, Nick Galbreath -Copyright (c) 2015 The Chromium Authors -Copyright (c) 2018 Alexander Chermyanin -Copyright (c) The Internet Society 1997 -Copyright (c) 2004-2006 Intel Corporation -Copyright (c) 2011-2015 Intel Corporation -Copyright (c) 2013-2017, Milosz Krajewski -Copyright (c) 2016-2017, Matthieu Darbois -Copyright (c) The Internet Society (2003) -Copyright (c) .NET Foundation Contributors -(c) 1995-2024 Jean-loup Gailly and Mark Adler -Copyright (c) 2020 Mara Bos -Copyright (c) .NET Foundation and Contributors -Copyright (c) 2012 - present, Victor Zverovich -Copyright (c) 2006 Jb Evain (jbevain@gmail.com) -Copyright (c) 2008-2020 Advanced Micro Devices, Inc. -Copyright (c) 2019 Microsoft Corporation, Daan Leijen -Copyright (c) 2011 Novell, Inc (http://www.novell.com) -Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) -Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors -Copyright (c) 2014 Ryan Juckett http://www.ryanjuckett.com -Copyright 1995-2024 Jean-loup Gailly and Mark Adler Qkkbal -Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. -Portions (c) International Organization for Standardization 1986 -Copyright (c) YEAR W3C(r) (MIT, ERCIM, Keio, Beihang) Disclaimers -Copyright (c) 2015 THL A29 Limited, a Tencent company, and Milo Yip -Copyright (c) 1980, 1986, 1993 The Regents of the University of California -Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the University of California -Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & Digital Equipment Corporation, Maynard, Mass - -The MIT License (MIT) - -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - --------------------------------------------------------- --------------------------------------------------------- From e5cb458e67b18bb8082f0b79a9f4b3978cf5451b Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 12 Mar 2026 20:42:24 -0700 Subject: [PATCH 05/23] Ship initrd.img in MSI using build-time generation via powershell script (#14424) * Ship initrd.img in MSI using build-time generation via tar.exe Replace the install-time CreateInitrd/RemoveInitrd custom actions with a build-time step that generates initrd.img using the Windows built-in tar.exe (libarchive/bsdtar) and ships it directly in the MSI. The install-time approach had a race condition: wsl.exe could launch before the CreateInitrd custom action completed, causing ERROR_FILE_NOT_FOUND for initrd.img. Changes: - Add CMake custom command to generate initrd.img via tar.exe --format=newc - Add initrd.img as a regular file in the MSI tools component - Remove CreateInitrd/RemoveInitrd custom actions from WiX, DllMain, and wslinstall.def - Remove CreateCpioInitrd helper and its tests (no longer needed) - Update pipeline build targets to build initramfs instead of init * pr feedback * more pr feedback * switch to using a powershell script instead of tar.exe * powershell script feedback * hopefully final pr feedback --------- Co-authored-by: Ben Hillis --- .pipelines/build-stage.yml | 2 +- msipackage/CMakeLists.txt | 4 +- msipackage/package.wix.in | 22 +---- src/linux/init/CMakeLists.txt | 11 +++ src/windows/common/filesystem.cpp | 112 -------------------------- src/windows/common/filesystem.hpp | 5 -- src/windows/wslinstall/DllMain.cpp | 40 --------- src/windows/wslinstall/wslinstall.def | 2 - test/windows/InstallerTests.cpp | 51 ------------ test/windows/SimpleTests.cpp | 55 ------------- tools/create-initrd.ps1 | 67 +++++++++++++++ 11 files changed, 82 insertions(+), 289 deletions(-) create mode 100644 tools/create-initrd.ps1 diff --git a/.pipelines/build-stage.yml b/.pipelines/build-stage.yml index 9b3fc58bb..09bc7620f 100644 --- a/.pipelines/build-stage.yml +++ b/.pipelines/build-stage.yml @@ -23,7 +23,7 @@ parameters: - name: targets type: object default: - - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;init;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin" + - target: "wsl;libwsl;wslg;wslservice;wslhost;wslrelay;wslinstaller;wslinstall;initramfs;wslserviceproxystub;wslsettings;wslinstallerproxystub;testplugin" pattern: "wsl.exe,libwsl.dll,wslg.exe,wslservice.exe,wslhost.exe,wslrelay.exe,wslinstaller.exe,wslinstall.dll,wslserviceproxystub.dll,wslsettings/wslsettings.dll,wslsettings/wslsettings.exe,wslinstallerproxystub.dll,wsldevicehost.dll,WSLDVCPlugin.dll,testplugin.dll,wsldeps.dll" - target: "msixgluepackage" pattern: "gluepackage.msix" diff --git a/msipackage/CMakeLists.txt b/msipackage/CMakeLists.txt index c37a45414..a7c7c8c04 100644 --- a/msipackage/CMakeLists.txt +++ b/msipackage/CMakeLists.txt @@ -22,7 +22,7 @@ foreach(binary ${WINDOWS_BINARIES}) list(APPEND BINARIES_DEPENDENCIES "${PACKAGE_INPUT_DIR}/${binary}") endforeach() -set(LINUX_BINARIES init) +set(LINUX_BINARIES init;initrd.img) foreach(binary ${LINUX_BINARIES}) list(APPEND BINARIES_DEPENDENCIES "${BIN}/${binary}") endforeach() @@ -52,7 +52,7 @@ add_custom_command( add_custom_target(msipackage DEPENDS ${OUTPUT_PACKAGE}) set_target_properties(msipackage PROPERTIES EXCLUDE_FROM_ALL FALSE SOURCES ${PACKAGE_WIX_IN}) -add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init wslinstall msixgluepackage) +add_dependencies(msipackage wsl wslg wslservice wslhost wslrelay wslserviceproxystub init initramfs wslinstall msixgluepackage) if (WSL_BUILD_WSL_SETTINGS) add_dependencies(msipackage wslsettings libwsl) diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index 92bcb97a1..6ebc9f3b2 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -261,6 +261,7 @@ + @@ -461,21 +462,6 @@ Execute="deferred" /> - - - - - - - - - diff --git a/src/linux/init/CMakeLists.txt b/src/linux/init/CMakeLists.txt index 602896934..d5dee8005 100644 --- a/src/linux/init/CMakeLists.txt +++ b/src/linux/init/CMakeLists.txt @@ -51,3 +51,14 @@ add_linux_executable(init "${SOURCES}" "${HEADERS};${COMMON_LINUX_HEADERS}" "${I add_dependencies(init localization) set_target_properties(init PROPERTIES FOLDER linux) + +set(INITRAMFS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/initrd.img) +set(INIT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${CMAKE_BUILD_TYPE}/init) +add_custom_command( + OUTPUT ${INITRAMFS} "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/initramfs" + DEPENDS init ${INIT} + COMMAND powershell.exe -ExecutionPolicy Bypass -NoProfile -NonInteractive -File "${CMAKE_SOURCE_DIR}/tools/create-initrd.ps1" "${INIT}" "${INITRAMFS}" + COMMAND ${CMAKE_COMMAND} -E touch "${CMAKE_CURRENT_BINARY_DIR}/CmakeFiles/initramfs" + VERBATIM) +add_custom_target(initramfs DEPENDS ${INITRAMFS}) +set_target_properties(initramfs PROPERTIES FOLDER linux) diff --git a/src/windows/common/filesystem.cpp b/src/windows/common/filesystem.cpp index a4418d552..2c6c585d6 100644 --- a/src/windows/common/filesystem.cpp +++ b/src/windows/common/filesystem.cpp @@ -1115,118 +1115,6 @@ void wsl::windows::common::filesystem::UpdateInit(_In_ PCWSTR BasePath, _In_ ULO CopyFileWithMetadata(source.c_str(), dest.c_str(), (LX_S_IFREG | 0755), DistroVersion); } -void wsl::windows::common::filesystem::CreateCpioInitrd(_In_ const std::filesystem::path& SourcePath, _In_ const std::filesystem::path& DestPath) -{ - // Open the source init binary - wil::unique_hfile sourceFile{ - CreateFileW(SourcePath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; - THROW_LAST_ERROR_IF(!sourceFile); - - // Get file size - LARGE_INTEGER fileSize{}; - THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(sourceFile.get(), &fileSize)); - THROW_HR_IF(E_INVALIDARG, fileSize.HighPart != 0); // File too large - - const DWORD initSize = fileSize.LowPart; - const auto initDataPadding = (4 - (initSize % 4)) % 4; - wil::unique_hfile destFile{CreateFileW(DestPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)}; - THROW_LAST_ERROR_IF(!destFile); - - // Clean up the destination file on failure. - auto deleteFile = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { - destFile.reset(); - LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(DestPath.c_str())); - }); - - auto writeCpioHeader = [&destFile](DWORD fileSize, PCSTR name) { - // CPIO newc header: magic(6) + 13 fields of 8 hex chars each = 110 bytes - constexpr size_t headerSize = 110; - const auto nameLen = strlen(name) + 1; - const auto headerPadding = (4 - ((headerSize + nameLen) % 4)) % 4; - const bool isTrailer = strcmp(name, "TRAILER!!!") == 0; - - // Get current time for mtime. CPIO newc format only supports 32-bit fields. - const auto mtime = static_cast(time(nullptr)); - - char header[headerSize + 1]; - sprintf_s( - header, - "070701" // magic - "%08X" // inode - "%08X" // mode - "%08X" // uid - "%08X" // gid - "%08X" // nlink - "%08X" // mtime - "%08X" // filesize - "%08X" // devmajor - "%08X" // devminor - "%08X" // rdevmajor - "%08X" // rdevminor - "%08X" // namesize - "%08X", // check - 0, - isTrailer ? 0 : 0100755, - 0, - 0, - isTrailer ? 0 : 1, - mtime, - fileSize, - 0, - 0, - 0, - 0, - static_cast(nameLen), - 0); - - DWORD bytesWritten; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), header, headerSize, &bytesWritten, nullptr)); - - // Write name with null terminator - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), name, static_cast(nameLen), &bytesWritten, nullptr)); - - // Write padding to align to 4 bytes - if (headerPadding > 0) - { - char padding[4] = {0}; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), padding, static_cast(headerPadding), &bytesWritten, nullptr)); - } - }; - - // Write init file header - writeCpioHeader(initSize, SourcePath.filename().string().c_str()); - - // Copy file contents - wsl::windows::common::relay::InterruptableRelay(sourceFile.get(), destFile.get(), nullptr, 64 * 1024); - - // Write data padding - if (initDataPadding > 0) - { - char padding[4] = {0}; - DWORD bytesWritten; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), padding, static_cast(initDataPadding), &bytesWritten, nullptr)); - } - - // Write trailer entry (empty file with name "TRAILER!!!") - writeCpioHeader(0, "TRAILER!!!"); - - // Pad the archive to 512-byte boundary - constexpr DWORD archiveBlockSize = 512; - LARGE_INTEGER currentPos{}; - THROW_IF_WIN32_BOOL_FALSE(SetFilePointerEx(destFile.get(), {}, ¤tPos, FILE_CURRENT)); - - const auto currentSize = static_cast(currentPos.QuadPart); - const auto archivePadding = static_cast((archiveBlockSize - (currentSize % archiveBlockSize)) % archiveBlockSize); - if (archivePadding > 0) - { - char paddingBuffer[archiveBlockSize] = {0}; - DWORD bytesWritten; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(destFile.get(), paddingBuffer, archivePadding, &bytesWritten, nullptr)); - } - - deleteFile.release(); -} - wil::unique_hfile wsl::windows::common::filesystem::WipeAndOpenDirectory(_In_ LPCWSTR pPath) { const auto result = wil::RemoveDirectoryRecursiveNoThrow(pPath); diff --git a/src/windows/common/filesystem.hpp b/src/windows/common/filesystem.hpp index 865f6ca3f..1a89f8b8f 100644 --- a/src/windows/common/filesystem.hpp +++ b/src/windows/common/filesystem.hpp @@ -228,11 +228,6 @@ std::wstring UnquotePath(_In_ LPCWSTR Path); /// void UpdateInit(_In_ PCWSTR BasePath, _In_ ULONG DistroVersion); -/// -/// Creates a CPIO newc format initrd archive from a single source file. -/// -void CreateCpioInitrd(_In_ const std::filesystem::path& SourcePath, _In_ const std::filesystem::path& DestPath); - /// /// Wipes out the directory with the given path if it exists, then creates it again and returns /// an open directory handle onto it. diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp index 119e420c5..37cc1539e 100644 --- a/src/windows/wslinstall/DllMain.cpp +++ b/src/windows/wslinstall/DllMain.cpp @@ -872,46 +872,6 @@ extern "C" UINT __stdcall UnregisterLspCategories(MSIHANDLE install) return NOERROR; } -extern "C" UINT __stdcall CreateInitrd(MSIHANDLE install) -try -{ - WSL_INSTALL_LOG("CreateInitrd"); - - const auto installRoot = wsl::windows::common::wslutil::GetMsiPackagePath(); - THROW_HR_IF(E_INVALIDARG, !installRoot.has_value()); - - const auto toolsPath = std::filesystem::path(installRoot.value()) / LXSS_TOOLS_DIRECTORY; - const auto initPath = toolsPath / L"init"; - const auto initrdPath = toolsPath / LXSS_VM_MODE_INITRD_NAME; - wsl::windows::common::filesystem::CreateCpioInitrd(initPath, initrdPath); - - return NOERROR; -} -catch (...) -{ - LOG_CAUGHT_EXCEPTION(); - - return ERROR_INSTALL_FAILURE; -} - -extern "C" UINT __stdcall RemoveInitrd(MSIHANDLE install) -{ - try - { - WSL_INSTALL_LOG("RemoveInitrd"); - - const auto installRoot = wsl::windows::common::wslutil::GetMsiPackagePath(); - THROW_HR_IF(E_INVALIDARG, !installRoot.has_value()); - - const auto initrdPath = std::filesystem::path(installRoot.value()) / LXSS_TOOLS_DIRECTORY / LXSS_VM_MODE_INITRD_NAME; - THROW_IF_WIN32_BOOL_FALSE(DeleteFileW(initrdPath.c_str())); - } - CATCH_LOG() - - // Failures in this method aren't fatal. - return NOERROR; -} - std::wstring GetWslSettingsInstalledExePath(MSIHANDLE install) { const auto wslSettingsInstallFolder = GetMsiProperty(install, c_wslSettingsInstalledDirectoryPropertyName); diff --git a/src/windows/wslinstall/wslinstall.def b/src/windows/wslinstall/wslinstall.def index 548614a54..ec3577ff4 100644 --- a/src/windows/wslinstall/wslinstall.def +++ b/src/windows/wslinstall/wslinstall.def @@ -4,8 +4,6 @@ EXPORTS CleanExplorerState CleanMsixState DeprovisionMsix - CreateInitrd - RemoveInitrd WslValidateInstallation WslFinalizeInstallation InstallMsix diff --git a/test/windows/InstallerTests.cpp b/test/windows/InstallerTests.cpp index dfa5f770a..c578ba91a 100644 --- a/test/windows/InstallerTests.cpp +++ b/test/windows/InstallerTests.cpp @@ -485,57 +485,6 @@ class InstallerTests ValidatePackageInstalledProperly(); } - TEST_METHOD(InitrdImgLifecycle) - { - // initrd.img is generated during installation from the init binary. - // It should: - // 1. Exist after a fresh install (generated by custom action) - // 2. Be re-created when the component is upgraded - // 3. Persist when reinstalling the same version - // 4. Be removed on uninstall - - auto initrdPath = m_installedPath / L"tools" / L"initrd.img"; - - // Uninstall and install an older version to get a clean state - UninstallMsi(); - VERIFY_IS_FALSE(IsMsiPackageInstalled()); - - // Case 1: Install older version (2.5.10 which ships initrd.img in the MSI) - InstallGitHubRelease(L"2.5.10"); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after installing 2.5.10"); - ValidatePackageInstalledProperly(L"2.5.10.0"); - - // Case 2: Upgrade to current version - initrd.img should be regenerated by custom action - InstallMsi(); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after upgrade (generated by installer)"); - ValidatePackageInstalledProperly(); - - // Case 3: Reinstall same version - initrd.img should persist - InstallMsi(); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after reinstalling same version"); - ValidatePackageInstalledProperly(); - - // Case 4: Downgrade to older version - InstallGitHubRelease(L"2.5.10"); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after downgrade"); - ValidatePackageInstalledProperly(L"2.5.10.0"); - - // Case 5: Uninstall - initrd.img should be removed - UninstallMsi(); - VERIFY_IS_FALSE(IsMsiPackageInstalled()); - VERIFY_IS_FALSE(std::filesystem::exists(initrdPath), L"initrd.img should be removed on uninstall"); - - // Reinstall to leave the system in a consistent state for other tests - InstallMsi(); - VERIFY_IS_TRUE(IsMsiPackageInstalled()); - VERIFY_IS_TRUE(std::filesystem::exists(initrdPath), L"initrd.img should exist after fresh install"); - ValidatePackageInstalledProperly(); - } - TEST_METHOD(UpgradeInstallsTheMsiPackage) { // Remove the MSIX package diff --git a/test/windows/SimpleTests.cpp b/test/windows/SimpleTests.cpp index fff708d9e..048787df4 100644 --- a/test/windows/SimpleTests.cpp +++ b/test/windows/SimpleTests.cpp @@ -296,60 +296,5 @@ class SimpleTests VERIFY_IS_TRUE(output.find(L"/mnt/c/Program Files (x86)/Common Files") != std::wstring::npos); VERIFY_IS_TRUE(output.find(L"/mnt/c/Users/Test User/AppData/Local/Programs/Microsoft VS Code/bin") != std::wstring::npos); } - - TEST_METHOD(CreateCpioInitrd) - { - using wsl::windows::common::filesystem::CreateCpioInitrd; - using wsl::windows::common::filesystem::TempFile; - using wsl::windows::common::filesystem::TempFileFlags; - - auto validateCpio = [](size_t sourceSize) { - // Create source file with specified size - TempFile sourceFile(GENERIC_WRITE, 0, CREATE_ALWAYS, TempFileFlags::None); - std::vector sourceData(sourceSize, 'X'); - DWORD written; - THROW_IF_WIN32_BOOL_FALSE( - WriteFile(sourceFile.Handle.get(), sourceData.data(), static_cast(sourceData.size()), &written, nullptr)); - sourceFile.Handle.reset(); - - // Create CPIO archive - TempFile destFile(0, 0, CREATE_ALWAYS, TempFileFlags::None, L"img"); - CreateCpioInitrd(sourceFile.Path, destFile.Path); - - // Read and validate the CPIO archive - wil::unique_hfile cpioHandle{CreateFileW( - destFile.Path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; - THROW_LAST_ERROR_IF(!cpioHandle); - - LARGE_INTEGER cpioSize{}; - THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(cpioHandle.get(), &cpioSize)); - VERIFY_ARE_EQUAL(cpioSize.QuadPart % 512, 0LL); // Archive padded to 512-byte boundary - - char header[111] = {}; - DWORD bytesRead; - THROW_IF_WIN32_BOOL_FALSE(ReadFile(cpioHandle.get(), header, 110, &bytesRead, nullptr)); - VERIFY_ARE_EQUAL(bytesRead, 110u); - - // Parse CPIO newc header: magic(6) ino mode uid gid nlink mtime filesize devmajor devminor rdevmajor rdevminor namesize check - DWORD fileSize, nameSize; - VERIFY_ARE_EQUAL(sscanf_s(header, "070701%*8x%*8x%*8x%*8x%*8x%*8x%8x%*8x%*8x%*8x%*8x%8x", &fileSize, &nameSize), 2); - - // Verify filename matches source file - auto expectedName = sourceFile.Path.filename().string(); - VERIFY_ARE_EQUAL(nameSize, static_cast(expectedName.size() + 1)); - - std::string filename(nameSize, '\0'); - THROW_IF_WIN32_BOOL_FALSE(ReadFile(cpioHandle.get(), filename.data(), nameSize, &bytesRead, nullptr)); - VERIFY_ARE_EQUAL(filename.c_str(), expectedName); - - VERIFY_ARE_EQUAL(fileSize, static_cast(sourceSize)); - }; - - // Test various sizes to exercise 4-byte alignment padding - for (size_t size : {0, 1, 2, 3, 4, 5, 100, 1024, 4096, 65536}) - { - validateCpio(size); - } - } }; } // namespace SimpleTests \ No newline at end of file diff --git a/tools/create-initrd.ps1 b/tools/create-initrd.ps1 new file mode 100644 index 000000000..f3bca1a8f --- /dev/null +++ b/tools/create-initrd.ps1 @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Creates a CPIO newc format initramfs archive containing a single file named "init" +# with mode 0100755 (rwxr-xr-x), uid 0, gid 0. + +[CmdletBinding()] +param ( + [Parameter(Mandatory)][string]$InputFile, + [Parameter(Mandatory)][string]$OutputFile +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +function Write-Pad([System.IO.Stream]$Stream) +{ + $remainder = $Stream.Position % 4 + if ($remainder -ne 0) + { + $pad = [byte[]]::new(4 - $remainder) + $Stream.Write($pad, 0, $pad.Length) + } +} + +function Write-CpioEntry([System.IO.Stream]$Stream, [byte[]]$NameBytes, [byte[]]$FileData, [int]$Mode, [uint32]$Mtime) +{ + $header = "070701" + # header magic + "00000001" + # inode + ("{0:X8}" -f $Mode) + # mode + "00000000" + # uid + "00000000" + # gid + "00000001" + # nlink + ("{0:X8}" -f $Mtime) + # mtime + ("{0:X8}" -f $FileData.Length) + # filesize + "00000000" + # devmajor + "00000000" + # devminor + "00000000" + # rdevmajor + "00000000" + # rdevminor + ("{0:X8}" -f $NameBytes.Length) + # namesize + "00000000" # check + $headerBytes = [System.Text.Encoding]::ASCII.GetBytes($header) + $Stream.Write($headerBytes, 0, $headerBytes.Length) + $Stream.Write($NameBytes, 0, $NameBytes.Length) + Write-Pad $Stream + if ($FileData.Length -gt 0) + { + $Stream.Write($FileData, 0, $FileData.Length) + Write-Pad $Stream + } +} + +$data = [System.IO.File]::ReadAllBytes($InputFile) +$name = [System.Text.Encoding]::ASCII.GetBytes("init`0") +$mtime = [uint32][System.DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + +$out = [System.IO.File]::Create($OutputFile) +try +{ + Write-CpioEntry $out $name $data 0x81ED $mtime # S_IFREG | 0755 + $trailer = [System.Text.Encoding]::ASCII.GetBytes("TRAILER!!!`0") + Write-CpioEntry $out $trailer ([byte[]]::new(0)) 0 0 +} +finally +{ + $out.Close() +} From 81dc9a3004063b91524ece24a85bfbc8e0b81ac2 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 13 Mar 2026 10:18:34 -0700 Subject: [PATCH 06/23] virtiofs: update logic so querying virtiofs mount source does not require a call to the service (#14380) * virtiofs: update logic so querying virtiofs mount source does not require a call to the service * more pr feedback * use std::filesystem::read_symlink * pr feedback and use canonical path in virtiofs symlink * make sure canonical path is always used --------- Co-authored-by: Ben Hillis --- src/linux/init/drvfs.cpp | 106 ++++++++++++++++++++------ src/shared/inc/lxinitshared.h | 15 +--- src/windows/service/exe/WslCoreVm.cpp | 40 ++++------ src/windows/service/exe/WslCoreVm.h | 2 +- 4 files changed, 100 insertions(+), 63 deletions(-) diff --git a/src/linux/init/drvfs.cpp b/src/linux/init/drvfs.cpp index 943557175..c36ae7e81 100644 --- a/src/linux/init/drvfs.cpp +++ b/src/linux/init/drvfs.cpp @@ -21,6 +21,7 @@ Module Name: #include "config.h" #include "message.h" #include +#include #include using namespace std::chrono_literals; @@ -32,6 +33,8 @@ using namespace std::chrono_literals; #define PLAN9_SYMLINK_ROOT_OPTION "symlinkroot=" #define PLAN9_UNC_PREFIX_LENGTH (2) +#define VIRTIOFS_TAG_DIR "/run/wsl/virtiofs" + #define LOG_STDERR(_errno) fprintf(stderr, "mount: %s\n", strerror(_errno)) constexpr int c_exitCodeInvalidUsage = 1; @@ -41,6 +44,62 @@ int MountFilesystem(const char* FsType, const char* Source, const char* Target, int MountWithRetry(const char* Source, const char* Target, const char* FsType, const char* Options, int* ExitCode = nullptr); +void SaveVirtiofsTagMapping(const char* Tag, const char* Source) + +/*++ + +Routine Description: + + This routine creates a symlink in VIRTIOFS_TAG_DIR that maps a virtiofs tag + to its Windows mount source path. This allows QueryVirtiofsMountSource to + resolve tags without talking to the service. + +Arguments: + + Tag - Supplies the virtiofs tag. + + Source - Supplies the Windows path the tag refers to. + +Return Value: + + None. + +--*/ + +{ + // + // Validate the tag is a GUID to prevent path traversal. + // + + const auto Guid = wsl::shared::string::ToGuid(Tag); + if (!Guid) + { + LOG_WARNING("Invalid virtiofs tag {}", Tag); + return; + } + + // + // Canonicalize path separators to backslashes before persisting. + // + + std::string CanonicalSource{Source}; + UtilCanonicalisePathSeparator(CanonicalSource, PATH_SEP_NT); + + UtilMkdirPath(VIRTIOFS_TAG_DIR, 0755); + + auto LinkPath = std::format("{}/{}", VIRTIOFS_TAG_DIR, Tag); + + // + // Remove any existing symlink for this tag before creating a new one. + // + + unlink(LinkPath.c_str()); + if (symlink(CanonicalSource.c_str(), LinkPath.c_str()) < 0) + { + LOG_WARNING("Failed to create virtiofs tag symlink {} -> {}: {}", LinkPath, CanonicalSource, errno); + } +} + std::pair ConvertDrvfsMountOptionsToPlan9(std::string_view Options, const wsl::linux::WslDistributionConfig& Config) /*++ @@ -565,7 +624,18 @@ try // auto* Tag = wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); - return MountWithRetry(Tag, Target, VIRTIO_FS_TYPE, MountOptions.c_str(), ExitCode); + auto* ResponseSource = wsl::shared::string::FromSpan(ResponseSpan, Response.SourceOffset); + THROW_LAST_ERROR_IF(MountWithRetry(Tag, Target, VIRTIO_FS_TYPE, MountOptions.c_str(), ExitCode) < 0); + + // + // Save the tag mapping. + // + // N.B. Use the source path from the response since the service canonicalizes it. + // + + SaveVirtiofsTagMapping(Tag, ResponseSource); + + return 0; } CATCH_RETURN_ERRNO() @@ -620,8 +690,13 @@ try return -1; } - Tag = wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); - return MountWithRetry(Tag, Target, VIRTIO_FS_TYPE, Options); + auto* NewTag = wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); + auto* Source = wsl::shared::string::FromSpan(ResponseSpan, Response.SourceOffset); + THROW_LAST_ERROR_IF(MountWithRetry(NewTag, Target, VIRTIO_FS_TYPE, Options) < 0); + + SaveVirtiofsTagMapping(NewTag, Source); + + return 0; } CATCH_RETURN_ERRNO() @@ -631,7 +706,8 @@ std::string QueryVirtiofsMountSource(const char* Tag) Routine Description: - This routine takes a virtiofs tag and determines the Windows path it refers to. + This routine takes a virtiofs tag and determines the Windows path it refers to + by reading the symlink created during mount. Arguments: @@ -660,28 +736,12 @@ try return {}; } - wsl::shared::MessageWriter QueryShare(LxInitMessageQueryVirtioFsDevice); - QueryShare.WriteString(QueryShare->TagOffset, Tag); - // - // Connect to the host and send the query request. + // Read the symlink that maps this tag to its Windows source path. // - wsl::shared::SocketChannel Channel{UtilConnectVsock(LX_INIT_UTILITY_VM_VIRTIOFS_PORT, true), "QueryVirtioFs"}; - if (Channel.Socket() < 0) - { - return {}; - } - - gsl::span ResponseSpan; - const auto& Response = Channel.Transaction(QueryShare.Span(), &ResponseSpan); - if (Response.Result != 0) - { - LOG_ERROR("Query virtiofs share for {} failed {}", Tag, Response.Result); - return {}; - } - - return wsl::shared::string::FromSpan(ResponseSpan, Response.TagOffset); + auto LinkPath = std::format("{}/{}", VIRTIOFS_TAG_DIR, Tag); + return std::filesystem::read_symlink(LinkPath).string(); } catch (...) { diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h index 14bf7b886..0e0ba26e0 100644 --- a/src/shared/inc/lxinitshared.h +++ b/src/shared/inc/lxinitshared.h @@ -302,7 +302,6 @@ typedef enum _LX_MESSAGE_TYPE LxInitMessageAddVirtioFsDevice, LxInitMessageAddVirtioFsDeviceResponse, LxInitMessageRemountVirtioFsDevice, - LxInitMessageQueryVirtioFsDevice, LxInitMessageStartDistroInit, LxInitMessageCreateLoginSession, LxInitMessageStopPlan9Server, @@ -1073,9 +1072,10 @@ typedef struct _LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE MESSAGE_HEADER Header; int Result; unsigned int TagOffset; + unsigned int SourceOffset; char Buffer[]; - PRETTY_PRINT(FIELD(Header), FIELD(Result), STRING_FIELD(TagOffset)); + PRETTY_PRINT(FIELD(Header), FIELD(Result), STRING_FIELD(TagOffset), STRING_FIELD(SourceOffset)); } LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE, *PLX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE; typedef struct _LX_INIT_ADD_VIRTIOFS_SHARE_MESSAGE @@ -1105,17 +1105,6 @@ typedef struct _LX_INIT_REMOUNT_VIRTIOFS_SHARE_MESSAGE PRETTY_PRINT(FIELD(Header), FIELD(Admin), STRING_FIELD(TagOffset)); } LX_INIT_REMOUNT_VIRTIOFS_SHARE_MESSAGE, *PLX_INIT_REMOUNT_VIRTIOFS_SHARE_MESSAGE; -typedef struct _LX_INIT_QUERY_VIRTIOFS_SHARE_MESSAGE -{ - static inline auto Type = LxInitMessageQueryVirtioFsDevice; - using TResponse = LX_INIT_ADD_VIRTIOFS_SHARE_RESPONSE_MESSAGE; - - MESSAGE_HEADER Header; - unsigned int TagOffset; - char Buffer[]; - - PRETTY_PRINT(FIELD(Header), STRING_FIELD(TagOffset)); -} LX_INIT_QUERY_VIRTIOFS_SHARE_MESSAGE, *PLX_INIT_QUERY_VIRTIOFS_SHARE_MESSAGE; // // The messages that can be sent to mini_init. // diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index 39552dd9c..6c4ec892a 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -2128,7 +2128,7 @@ void WslCoreVm::WaitForPmemDeviceInVm(_In_ ULONG PmemId) } _Requires_lock_held_(m_guestDeviceLock) -std::wstring WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken) +std::pair WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken) { WI_ASSERT(m_vmConfig.EnableVirtioFs); @@ -2190,7 +2190,7 @@ std::wstring WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ TraceLoggingValue(created, "created"), TraceLoggingValue(m_virtioFsShares.size(), "shareCount")); - return tag; + return {tag, sharePath}; } void WslCoreVm::OnCrash(_In_ LPCWSTR Details) @@ -2581,12 +2581,13 @@ try return; } - auto respondWithTag = [&](const std::wstring& tag, HRESULT result) { + auto respondWithTag = [&](const std::wstring& tag, const std::wstring& source, HRESULT result) { // Respond to the guest with the tag that should be used to mount the device. wsl::shared::MessageWriter response(LxInitMessageAddVirtioFsDeviceResponse); response->Result = SUCCEEDED(result) ? 0 : EINVAL; // TODO: Improved HRESULT -> errno mapping. response.WriteString(response->TagOffset, tag); + response.WriteString(response->SourceOffset, source); channel.SendMessage(response.Span()); }; @@ -2594,7 +2595,8 @@ try if (message->MessageType == LxInitMessageAddVirtioFsDevice) { std::wstring tag; - const auto result = wil::ResultFromException([this, span, &tag]() { + std::wstring source; + const auto result = wil::ResultFromException([this, span, &tag, &source]() { const auto* addShare = gslhelpers::try_get_struct(span); THROW_HR_IF(E_UNEXPECTED, !addShare); @@ -2605,15 +2607,16 @@ try // Acquire the lock and attempt to add the device. auto guestDeviceLock = m_guestDeviceLock.lock_exclusive(); - tag = AddVirtioFsShare(addShare->Admin, pathWide.c_str(), optionsWide.c_str()); + std::tie(tag, source) = AddVirtioFsShare(addShare->Admin, pathWide.c_str(), optionsWide.c_str()); }); - respondWithTag(tag, result); + respondWithTag(tag, source, result); } else if (message->MessageType == LxInitMessageRemountVirtioFsDevice) { std::wstring newTag; - const auto result = wil::ResultFromException([this, span, &newTag]() { + std::wstring source; + const auto result = wil::ResultFromException([this, span, &newTag, &source]() { const auto* remountShare = gslhelpers::try_get_struct(span); THROW_HR_IF(E_UNEXPECTED, !remountShare); @@ -2623,28 +2626,13 @@ try const auto foundShare = FindVirtioFsShare(tagWide.c_str(), !remountShare->Admin); THROW_HR_IF_MSG(E_UNEXPECTED, !foundShare.has_value(), "Unknown tag %ls", tagWide.c_str()); - newTag = AddVirtioFsShare(remountShare->Admin, foundShare->Path.c_str(), foundShare->OptionsString().c_str()); - }); - - respondWithTag(newTag, result); - } - else if (message->MessageType == LxInitMessageQueryVirtioFsDevice) - { - std::wstring newTag; - const auto result = wil::ResultFromException([this, span, &newTag]() { - const auto* query = gslhelpers::try_get_struct(span); - THROW_HR_IF(E_UNEXPECTED, !query); - - const std::string tag = wsl::shared::string::FromSpan(span, query->TagOffset); - const auto tagWide = wsl::shared::string::MultiByteToWide(tag); - auto guestDeviceLock = m_guestDeviceLock.lock_exclusive(); - const auto foundShare = FindVirtioFsShare(tagWide.c_str()); - THROW_HR_IF_MSG(E_UNEXPECTED, !foundShare.has_value(), "Unknown tag %ls", tagWide.c_str()); + std::tie(newTag, source) = + AddVirtioFsShare(remountShare->Admin, foundShare->Path.c_str(), foundShare->OptionsString().c_str()); - newTag = foundShare->Path; + WI_ASSERT(source == foundShare->Path); }); - respondWithTag(newTag, result); + respondWithTag(newTag, source, result); } else { diff --git a/src/windows/service/exe/WslCoreVm.h b/src/windows/service/exe/WslCoreVm.h index 6178c54b7..420492b05 100644 --- a/src/windows/service/exe/WslCoreVm.h +++ b/src/windows/service/exe/WslCoreVm.h @@ -183,7 +183,7 @@ class WslCoreVm void AddPlan9Share(_In_ PCWSTR AccessName, _In_ PCWSTR Path, _In_ UINT32 Port, _In_ wsl::windows::common::hcs::Plan9ShareFlags Flags, _In_ HANDLE UserToken, _In_ PCWSTR VirtIoTag); _Requires_lock_held_(m_guestDeviceLock) - std::wstring AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken = nullptr); + std::pair AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken = nullptr); _Requires_lock_held_(m_lock) ULONG AttachDiskLockHeld(_In_ PCWSTR Disk, _In_ DiskType Type, _In_ MountFlags Flags, _In_ std::optional Lun, _In_ bool IsUserDisk, _In_ HANDLE UserToken); From 7cd9ed9603632c55d703416bf53ae02348e2bb28 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Fri, 13 Mar 2026 16:36:21 -0700 Subject: [PATCH 07/23] virtio networking: add support for ipv6 (#14350) * VirtioProxy: Add IPv6 address, gateway, and route support - Add PreferredIpv6Address field and GetBestGatewayV6* methods to NetworkSettings - Extend GetHostEndpointSettings() to discover IPv6 unicast address and gateway - Add UpdateIpv6Address() using ModifyGuestEndpointSettingRequest - Push IPv6 default route to guest via UpdateDefaultRoute(AF_INET6) - Remove AF_INET6 early return in ModifyOpenPorts, use INETADDR_PORT() - Add EndpointRoute::DefaultRoute() static factory - Pass client_ip_ipv6 in devicehost options (not yet parsed by devicehost) - Remove gateway_ip from devicehost options (only needed for DHCP) - Include IPv6 DNS servers in non-tunneling DNS settings - Add ConfigurationV6 and DnsResolutionAAAA tests * cleanup and add more ipv6 tests * added test coverage and minor updates * clang format * pr feedback * format source * pr feedback * test fixes --------- Co-authored-by: Ben Hillis --- packages.config | 2 +- src/windows/common/VirtioNetworking.cpp | 142 ++++++--- src/windows/common/VirtioNetworking.h | 12 +- .../common/WslCoreNetworkEndpointSettings.cpp | 86 ++++-- .../common/WslCoreNetworkEndpointSettings.h | 66 ++++- src/windows/common/WslCoreNetworkingSupport.h | 2 +- src/windows/service/exe/WslCoreVm.cpp | 2 +- test/windows/NetworkTests.cpp | 269 ++++++++++++++++-- 8 files changed, 474 insertions(+), 107 deletions(-) diff --git a/packages.config b/packages.config index a0f2af944..8d687ad70 100644 --- a/packages.config +++ b/packages.config @@ -18,7 +18,7 @@ - + diff --git a/src/windows/common/VirtioNetworking.cpp b/src/windows/common/VirtioNetworking.cpp index b150b94ef..652f64987 100644 --- a/src/windows/common/VirtioNetworking.cpp +++ b/src/windows/common/VirtioNetworking.cpp @@ -56,7 +56,7 @@ void VirtioNetworking::TraceLoggingRundown() noexcept void VirtioNetworking::FillInitialConfiguration(LX_MINI_INIT_NETWORKING_CONFIGURATION& message) { message.NetworkingMode = LxMiniInitNetworkingModeVirtioProxy; - message.DisableIpv6 = false; + message.DisableIpv6 = WI_IsFlagClear(m_flags, VirtioNetworkingFlags::Ipv6); message.EnableDhcpClient = false; message.PortTrackerType = LX_MINI_INIT_PORT_TRACKER_TYPE::LxMiniInitPortTrackerTypeMirrored; } @@ -78,6 +78,11 @@ void NETIOAPI_API_ VirtioNetworking::OnNetworkConnectivityChange(PVOID context, HRESULT VirtioNetworking::HandlePortNotification(const SOCKADDR_INET& addr, int protocol, bool allocate) const noexcept { + if (addr.si_family == AF_INET6 && WI_IsFlagClear(m_flags, VirtioNetworkingFlags::Ipv6)) + { + return S_OK; + } + int result = 0; const auto ipAddress = (addr.si_family == AF_INET) ? reinterpret_cast(&addr.Ipv4.sin_addr) : reinterpret_cast(&addr.Ipv6.sin6_addr); @@ -132,19 +137,12 @@ int VirtioNetworking::ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& LOG_HR_MSG(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED), "Unsupported bind protocol %d", protocol); return 0; } - else if (addr.si_family == AF_INET6) - { - // The virtio net adapter does not yet support IPv6 packets, so any traffic would arrive via - // IPv4. If the caller wants IPv4 they will also likely listen on an IPv4 address, which will - // be handled as a separate callback to this same code. - return 0; - } auto lock = m_lock.lock_exclusive(); const auto server = m_guestDeviceManager->GetRemoteFileSystem(VIRTIO_NET_CLASS_ID, c_defaultDeviceTag); if (server) { - std::wstring portString = std::format(L"tag={};port_number={}", tag, addr.Ipv4.sin_port); + std::wstring portString = std::format(L"tag={};port_number={}", tag, INETADDR_PORT(reinterpret_cast(&addr))); if (protocol == IPPROTO_UDP) { portString += L";udp"; @@ -185,7 +183,13 @@ try std::wstring default_route = networkSettings->GetBestGatewayAddressString(); appendOption(L"gateway_ip", default_route); - appendOption(L"gateway_mac", networkSettings->GetBestGatewayMacAddress()); + appendOption(L"gateway_mac", networkSettings->GetBestGatewayMacAddress(AF_INET)); + + if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)) + { + appendOption(L"client_ip_ipv6", networkSettings->PreferredIpv6Address.AddressString); + appendOption(L"gateway_mac_ipv6", networkSettings->GetBestGatewayMacAddress(AF_INET6)); + } networking::DnsInfo currentDns{}; if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::DnsTunneling)) @@ -194,7 +198,9 @@ try } else { - currentDns = networking::HostDnsInfo::GetDnsSettings(networking::DnsSettingsFlags::IncludeVpn); + wsl::core::networking::DnsSettingsFlags dnsFlags = networking::DnsSettingsFlags::IncludeVpn; + WI_SetFlagIf(dnsFlags, networking::DnsSettingsFlags::IncludeIpv6Servers, WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)); + currentDns = networking::HostDnsInfo::GetDnsSettings(dnsFlags); } const auto minMtu = GetMinimumConnectedInterfaceMtu(); @@ -221,32 +227,16 @@ try } } - // Update IP address if needed. - if (!m_networkSettings || networkSettings->PreferredIpAddress != m_networkSettings->PreferredIpAddress) + UpdateIpv4Address(networkSettings->PreferredIpAddress); + if (WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)) { - UpdateIpAddress(networkSettings->PreferredIpAddress); + UpdateIpv6Address(networkSettings->PreferredIpv6Address); } - // Send default route update if needed. - if (default_route != m_trackedDefaultRoute) - { - m_trackedDefaultRoute = default_route; - UpdateDefaultRoute(default_route, AF_INET); - } - - // Send DNS update if needed. - if (currentDns != m_trackedDnsSettings) - { - m_trackedDnsSettings = currentDns; - UpdateDnsSettings(currentDns); - } + UpdateDefaultRoute(default_route); - // Send MTU update if needed. - if (minMtu && minMtu.value() != m_networkMtu) - { - m_networkMtu = minMtu.value(); - UpdateMtu(m_networkMtu); - } + UpdateDnsSettings(currentDns); + UpdateMtu(minMtu); m_networkSettings = std::move(networkSettings); } @@ -282,27 +272,46 @@ void VirtioNetworking::SetupLoopbackDevice() m_gnsChannel.SendNetworkDeviceMessage(loopbackType, ToJsonW(createLoopbackDevice).c_str()); } -void VirtioNetworking::UpdateDefaultRoute(const std::wstring& gateway, ADDRESS_FAMILY family) +void VirtioNetworking::SendDefaultRoute(const std::wstring& gateway, hns::ModifyRequestType requestType) { - if (gateway.empty()) + if (gateway.empty() || !m_adapterId.has_value()) { return; } wsl::shared::hns::Route route; route.NextHop = gateway; - route.DestinationPrefix = (family == AF_INET) ? LX_INIT_DEFAULT_ROUTE_PREFIX : LX_INIT_DEFAULT_ROUTE_V6_PREFIX; - route.Family = family; + route.DestinationPrefix = LX_INIT_DEFAULT_ROUTE_PREFIX; + route.Family = AF_INET; hns::ModifyGuestEndpointSettingRequest request; - request.RequestType = hns::ModifyRequestType::Add; + request.RequestType = requestType; request.ResourceType = hns::GuestEndpointResourceType::Route; request.Settings = route; m_gnsChannel.SendHnsNotification(ToJsonW(request).c_str(), m_adapterId.value()); } +void VirtioNetworking::UpdateDefaultRoute(const std::wstring& gateway) +{ + if (gateway == m_trackedDefaultRoute || !m_adapterId.has_value()) + { + return; + } + + SendDefaultRoute(m_trackedDefaultRoute, hns::ModifyRequestType::Remove); + m_trackedDefaultRoute = gateway; + SendDefaultRoute(gateway, hns::ModifyRequestType::Add); +} + void VirtioNetworking::UpdateDnsSettings(const networking::DnsInfo& dns) { + if (dns == m_trackedDnsSettings || !m_adapterId.has_value()) + { + return; + } + + m_trackedDnsSettings = dns; + hns::ModifyGuestEndpointSettingRequest notification{}; notification.RequestType = hns::ModifyRequestType::Update; notification.ResourceType = hns::GuestEndpointResourceType::DNS; @@ -310,9 +319,17 @@ void VirtioNetworking::UpdateDnsSettings(const networking::DnsInfo& dns) m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_adapterId.value()); } -void VirtioNetworking::UpdateIpAddress(const networking::EndpointIpAddress& ipAddress) +void VirtioNetworking::UpdateIpv4Address(const networking::EndpointIpAddress& ipAddress) { - // N.B. The MAC address is advertised with the virtio device so doesn't need to be explicitly set. + if (ipAddress == m_trackedIpv4Address || ipAddress.AddressString.empty() || !m_adapterId.has_value()) + { + return; + } + + m_trackedIpv4Address = ipAddress; + + // N.B. SendEndpointState triggers SetAdapterConfiguration on the Linux side + // which brings the interface UP and configures the full adapter state. hns::HNSEndpoint endpointProperties; endpointProperties.ID = m_adapterId.value(); endpointProperties.IPAddress = ipAddress.AddressString; @@ -320,12 +337,53 @@ void VirtioNetworking::UpdateIpAddress(const networking::EndpointIpAddress& ipAd m_gnsChannel.SendEndpointState(endpointProperties); } -void VirtioNetworking::UpdateMtu(ULONG mtu) +void VirtioNetworking::SendIpv6Address(const networking::EndpointIpAddress& ipAddress, hns::ModifyRequestType requestType) +{ + WI_ASSERT(WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)); + + if (ipAddress.AddressString.empty() || !m_adapterId.has_value()) + { + return; + } + + // The HNSEndpoint schema doesn't support IPv6 addresses, so use ModifyGuestEndpointSettingRequest. + hns::ModifyGuestEndpointSettingRequest request; + request.RequestType = requestType; + request.ResourceType = hns::GuestEndpointResourceType::IPAddress; + request.Settings.Address = ipAddress.AddressString; + request.Settings.Family = ipAddress.Address.si_family; + request.Settings.OnLinkPrefixLength = ipAddress.PrefixLength; + request.Settings.PreferredLifetime = ULONG_MAX; + m_gnsChannel.SendHnsNotification(ToJsonW(request).c_str(), m_adapterId.value()); +} + +void VirtioNetworking::UpdateIpv6Address(const networking::EndpointIpAddress& ipAddress) +{ + WI_ASSERT(WI_IsFlagSet(m_flags, VirtioNetworkingFlags::Ipv6)); + + if (ipAddress == m_trackedIpv6Address || !m_adapterId.has_value()) + { + return; + } + + SendIpv6Address(m_trackedIpv6Address, hns::ModifyRequestType::Remove); + m_trackedIpv6Address = ipAddress; + SendIpv6Address(ipAddress, hns::ModifyRequestType::Add); +} + +void VirtioNetworking::UpdateMtu(std::optional mtu) { + if (!mtu || mtu.value() == m_networkMtu || !m_adapterId.has_value()) + { + return; + } + + m_networkMtu = mtu.value(); + hns::ModifyGuestEndpointSettingRequest notification{}; notification.ResourceType = hns::GuestEndpointResourceType::Interface; notification.RequestType = hns::ModifyRequestType::Update; notification.Settings.Connected = true; - notification.Settings.NlMtu = mtu; + notification.Settings.NlMtu = m_networkMtu; m_gnsChannel.SendHnsNotification(ToJsonW(notification).c_str(), m_adapterId.value()); } diff --git a/src/windows/common/VirtioNetworking.h b/src/windows/common/VirtioNetworking.h index 697524a81..c0fdb27b7 100644 --- a/src/windows/common/VirtioNetworking.h +++ b/src/windows/common/VirtioNetworking.h @@ -15,6 +15,7 @@ enum class VirtioNetworkingFlags None = 0x0, LocalhostRelay = 0x1, DnsTunneling = 0x2, + Ipv6 = 0x4, }; DEFINE_ENUM_FLAG_OPERATORS(VirtioNetworkingFlags); @@ -43,10 +44,13 @@ class VirtioNetworking : public INetworkingEngine int ModifyOpenPorts(_In_ PCWSTR tag, _In_ const SOCKADDR_INET& addr, _In_ int protocol, _In_ bool isOpen) const; void RefreshGuestConnection() noexcept; void SetupLoopbackDevice(); - void UpdateDefaultRoute(const std::wstring& gateway, ADDRESS_FAMILY family); + void SendDefaultRoute(const std::wstring& gateway, wsl::shared::hns::ModifyRequestType requestType); + void SendIpv6Address(const networking::EndpointIpAddress& ipAddress, wsl::shared::hns::ModifyRequestType requestType); + void UpdateDefaultRoute(const std::wstring& gateway); void UpdateDnsSettings(const networking::DnsInfo& dns); - void UpdateIpAddress(const networking::EndpointIpAddress& ipAddress); - void UpdateMtu(ULONG mtu); + void UpdateIpv4Address(const networking::EndpointIpAddress& ipAddress); + void UpdateIpv6Address(const networking::EndpointIpAddress& ipAddress); + void UpdateMtu(std::optional mtu); mutable wil::srwlock m_lock; @@ -62,6 +66,8 @@ class VirtioNetworking : public INetworkingEngine ULONG m_networkMtu = 0; std::wstring m_trackedDeviceOptions; + networking::EndpointIpAddress m_trackedIpv4Address{}; + networking::EndpointIpAddress m_trackedIpv6Address{}; std::wstring m_trackedDefaultRoute; networking::DnsInfo m_trackedDnsSettings{}; diff --git a/src/windows/common/WslCoreNetworkEndpointSettings.cpp b/src/windows/common/WslCoreNetworkEndpointSettings.cpp index 37c0cc904..89737c01d 100644 --- a/src/windows/common/WslCoreNetworkEndpointSettings.cpp +++ b/src/windows/common/WslCoreNetworkEndpointSettings.cpp @@ -24,7 +24,9 @@ std::shared_ptr wsl::core::networking::G return std::make_shared( properties.InterfaceConstraint.InterfaceGuid, address, + EndpointIpAddress{}, route, + EndpointRoute{}, properties.MacAddress, properties.InterfaceConstraint.InterfaceIndex, properties.InterfaceConstraint.InterfaceMediaType); @@ -61,44 +63,84 @@ std::shared_ptr wsl::core::networking::G } if (firstIpv4Address) { - address.Address = *reinterpret_cast(firstIpv4Address->Address.lpSockaddr); + address.Address.Ipv4 = *reinterpret_cast(firstIpv4Address->Address.lpSockaddr); address.AddressString = windows::common::string::SockAddrInetToWstring(address.Address); address.PrefixLength = firstIpv4Address->OnLinkPrefixLength; } - EndpointRoute route{}; - PIP_ADAPTER_GATEWAY_ADDRESS nextGatewayAddress = bestInterface->FirstGatewayAddress; - while (nextGatewayAddress && nextGatewayAddress->Address.lpSockaddr->sa_family != AF_INET) + // Find the first global-scope (non-link-local) IPv6 unicast address. + EndpointIpAddress ipv6Address{}; + auto nextUnicastAddress = bestInterface->FirstUnicastAddress; + while (nextUnicastAddress) { - nextGatewayAddress = nextGatewayAddress->Next; + if (nextUnicastAddress->Address.lpSockaddr->sa_family == AF_INET6) + { + const auto& sin6 = *reinterpret_cast(nextUnicastAddress->Address.lpSockaddr); + if (!IN6_IS_ADDR_LINKLOCAL(&sin6.sin6_addr) && !IN6_IS_ADDR_LOOPBACK(&sin6.sin6_addr)) + { + ipv6Address.Address.Ipv6 = sin6; + ipv6Address.AddressString = windows::common::string::SockAddrInetToWstring(ipv6Address.Address); + ipv6Address.PrefixLength = nextUnicastAddress->OnLinkPrefixLength; + break; + } + } + nextUnicastAddress = nextUnicastAddress->Next; } - if (nextGatewayAddress) + + // Helper to find the first gateway address of a given family. + auto findGatewayAddress = [](PIP_ADAPTER_GATEWAY_ADDRESS list, ADDRESS_FAMILY family) -> PIP_ADAPTER_GATEWAY_ADDRESS { + while (list && list->Address.lpSockaddr->sa_family != family) + { + list = list->Next; + } + return list; + }; + + // Build IPv4 default route. + EndpointRoute route{}; + const auto v4Gateway = findGatewayAddress(bestInterface->FirstGatewayAddress, AF_INET); + if (v4Gateway) { - route.DestinationPrefix.PrefixLength = 0; - IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4); - route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS; - route.NextHop = *reinterpret_cast(nextGatewayAddress->Address.lpSockaddr); - route.NextHopString = windows::common::string::SockAddrInetToWstring(route.NextHop); + SOCKADDR_INET v4NextHop{}; + v4NextHop.Ipv4 = *reinterpret_cast(v4Gateway->Address.lpSockaddr); + route = EndpointRoute::DefaultRoute(AF_INET, v4NextHop); } else if (address.Address.si_family == AF_INET) { - IN_ADDR default_route{}; - default_route.s_addr = htonl((ntohl(address.Address.Ipv4.sin_addr.s_addr) & ~((1 << (32 - address.PrefixLength)) - 1)) | 1); - route.DestinationPrefix.PrefixLength = 0; - IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4); - route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS; - IN4ADDR_SETSOCKADDR(&route.NextHop.Ipv4, &default_route, 0); - route.NextHopString = windows::common::string::SockAddrInetToWstring(route.NextHop); + // Synthesize a gateway from the first host address in the subnet. + SOCKADDR_INET gatewayAddr{}; + gatewayAddr.si_family = AF_INET; + const uint32_t hostAddr = ntohl(address.Address.Ipv4.sin_addr.s_addr); + const uint32_t mask = (address.PrefixLength == 0) ? 0u : ~((1u << (32u - address.PrefixLength)) - 1u); + gatewayAddr.Ipv4.sin_addr.s_addr = htonl((hostAddr & mask) | 1u); + route = EndpointRoute::DefaultRoute(AF_INET, gatewayAddr); + } + + // Build IPv6 default route. + EndpointRoute v6Route{}; + const auto v6Gateway = findGatewayAddress(bestInterface->FirstGatewayAddress, AF_INET6); + if (v6Gateway) + { + SOCKADDR_INET v6NextHop{}; + v6NextHop.Ipv6 = *reinterpret_cast(v6Gateway->Address.lpSockaddr); + v6Route = EndpointRoute::DefaultRoute(AF_INET6, v6NextHop); } return std::make_shared( - bestInterface->NetworkGuid, address, route, macAddress, bestInterface->IfIndex, bestInterface->IfType); + bestInterface->NetworkGuid, + std::move(address), + std::move(ipv6Address), + std::move(route), + std::move(v6Route), + std::move(macAddress), + bestInterface->IfIndex, + bestInterface->IfType); } -std::wstring wsl::core::networking::NetworkSettings::GetBestGatewayMacAddress() const +std::wstring wsl::core::networking::NetworkSettings::GetBestGatewayMacAddress(ADDRESS_FAMILY addressFamily) const { - auto gatewayAddress = GetBestGatewayAddress(); - if (gatewayAddress.si_family != AF_INET) + auto gatewayAddress = GetBestGatewayAddress(addressFamily); + if (gatewayAddress.si_family != addressFamily) { return {}; } diff --git a/src/windows/common/WslCoreNetworkEndpointSettings.h b/src/windows/common/WslCoreNetworkEndpointSettings.h index 227fb63e9..0dd3a92a4 100644 --- a/src/windows/common/WslCoreNetworkEndpointSettings.h +++ b/src/windows/common/WslCoreNetworkEndpointSettings.h @@ -172,6 +172,27 @@ struct EndpointRoute EndpointRoute(const EndpointRoute&) = default; EndpointRoute& operator=(const EndpointRoute&) = default; + // Build a default route (0.0.0.0/0 or ::/0) with the given next hop. + static EndpointRoute DefaultRoute(ADDRESS_FAMILY family, const SOCKADDR_INET& nextHop) + { + EndpointRoute route{}; + route.Family = family; + route.DestinationPrefix.PrefixLength = 0; + if (family == AF_INET) + { + IN4ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv4); + route.DestinationPrefixString = LX_INIT_UNSPECIFIED_ADDRESS; + } + else + { + IN6ADDR_SETANY(&route.DestinationPrefix.Prefix.Ipv6); + route.DestinationPrefixString = LX_INIT_UNSPECIFIED_V6_ADDRESS; + } + route.NextHop = nextHop; + route.NextHopString = windows::common::string::SockAddrInetToWstring(nextHop); + return route; + } + EndpointRoute(const MIB_IPFORWARD_ROW2& RouteRow) : Family(RouteRow.NextHop.si_family), DestinationPrefix(RouteRow.DestinationPrefix), @@ -258,19 +279,39 @@ struct NetworkSettings { NetworkSettings() = default; - NetworkSettings(const GUID& interfaceGuid, EndpointIpAddress preferredIpAddress, EndpointRoute gateway, std::wstring macAddress, uint32_t interfaceIndex, uint32_t mediaType) : + NetworkSettings( + const GUID& interfaceGuid, + EndpointIpAddress preferredIpAddress, + EndpointIpAddress preferredIpv6Address, + EndpointRoute gateway, + EndpointRoute v6Gateway, + std::wstring macAddress, + uint32_t interfaceIndex, + uint32_t mediaType) : InterfaceGuid(interfaceGuid), PreferredIpAddress(std::move(preferredIpAddress)), + PreferredIpv6Address(std::move(preferredIpv6Address)), MacAddress(std::move(macAddress)), InterfaceIndex(interfaceIndex), InterfaceType(mediaType) { - Routes.emplace(std::move(gateway)); + // Only insert routes that have a valid next hop. A default-constructed or empty + // EndpointRoute indicates no gateway was found for that address family. + if (!gateway.NextHopString.empty()) + { + Routes.emplace(std::move(gateway)); + } + + if (!v6Gateway.NextHopString.empty()) + { + Routes.emplace(std::move(v6Gateway)); + } } GUID InterfaceGuid{}; EndpointIpAddress PreferredIpAddress{}; - std::set IpAddresses{}; // Does not include PreferredIpAddress. + EndpointIpAddress PreferredIpv6Address{}; + std::set IpAddresses{}; // Does not include PreferredIpAddress or PreferredIpv6Address. std::set Routes{}; std::wstring MacAddress; IF_INDEX InterfaceIndex = 0; @@ -290,12 +331,13 @@ struct NetworkSettings auto operator<=>(const NetworkSettings&) const = default; - std::wstring GetBestGatewayAddressString() const + // Returns the next-hop string of the first default route matching the given address family. + std::wstring GetBestGatewayAddressString(ADDRESS_FAMILY family = AF_INET) const { - // Best is currently defined as simply the first IPv4 gateway. + const auto& unspecified = (family == AF_INET) ? LX_INIT_UNSPECIFIED_ADDRESS : LX_INIT_UNSPECIFIED_V6_ADDRESS; for (const auto& route : Routes) { - if (route.Family == AF_INET && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == LX_INIT_UNSPECIFIED_ADDRESS) + if (route.Family == family && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == unspecified) { return route.NextHopString; } @@ -304,12 +346,13 @@ struct NetworkSettings return {}; } - SOCKADDR_INET GetBestGatewayAddress() const + // Returns the next-hop address of the first default route matching the given address family. + SOCKADDR_INET GetBestGatewayAddress(ADDRESS_FAMILY family = AF_INET) const { - // Best is currently defined as simply the first IPv4 gateway. + const auto& unspecified = (family == AF_INET) ? LX_INIT_UNSPECIFIED_ADDRESS : LX_INIT_UNSPECIFIED_V6_ADDRESS; for (const auto& route : Routes) { - if (route.Family == AF_INET && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == LX_INIT_UNSPECIFIED_ADDRESS) + if (route.Family == family && route.DestinationPrefix.PrefixLength == 0 && route.DestinationPrefixString == unspecified) { return route.NextHop; } @@ -318,7 +361,7 @@ struct NetworkSettings return {}; } - std::wstring GetBestGatewayMacAddress() const; + std::wstring GetBestGatewayMacAddress(ADDRESS_FAMILY addressFamily) const; std::wstring IpAddressesString() const { @@ -369,6 +412,9 @@ std::shared_ptr GetHostEndpointSettings(); TraceLoggingValue((settings)->GetBestGatewayAddressString().c_str(), "bestGatewayAddress"), \ TraceLoggingValue((settings)->PreferredIpAddress.AddressString.c_str(), "preferredIpAddress"), \ TraceLoggingValue((settings)->PreferredIpAddress.PrefixLength, "preferredIpAddressPrefixLength"), \ + TraceLoggingValue((settings)->PreferredIpv6Address.AddressString.c_str(), "preferredIpv6Address"), \ + TraceLoggingValue((settings)->PreferredIpv6Address.PrefixLength, "preferredIpv6AddressPrefixLength"), \ + TraceLoggingValue((settings)->GetBestGatewayAddressString(AF_INET6).c_str(), "bestGatewayV6Address"), \ TraceLoggingValue((settings)->IpAddressesString().c_str(), "ipAddresses"), \ TraceLoggingValue((settings)->RoutesString().c_str(), "routes"), \ TraceLoggingValue((settings)->MacAddress.c_str(), "macAddress"), \ diff --git a/src/windows/common/WslCoreNetworkingSupport.h b/src/windows/common/WslCoreNetworkingSupport.h index f66a74234..106167075 100644 --- a/src/windows/common/WslCoreNetworkingSupport.h +++ b/src/windows/common/WslCoreNetworkingSupport.h @@ -544,7 +544,7 @@ class AdapterAddresses : public std::enable_shared_from_this m_buffer.resize(BufferSize); Result = GetAdaptersAddresses( AF_UNSPEC, - (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST), + (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS), nullptr, (PIP_ADAPTER_ADDRESSES)m_buffer.data(), &BufferSize); diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index 6c4ec892a..952ff7f34 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -576,7 +576,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken } else if (m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy) { - wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::None; + wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6; WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay, m_vmConfig.EnableLocalhostRelay); m_networkingEngine = std::make_unique( std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken); diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 6df73175d..12cfd4fbd 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -2729,7 +2729,10 @@ class NetworkTests { if (std::regex_search(line, match, defaultRoutePattern) && match.size() >= 3) { - VERIFY_IS_FALSE(state.DefaultRoute.has_value()); + if (state.DefaultRoute.has_value()) + { + continue; + } state.DefaultRoute = {{match.str(1), match.str(2), {}, match.size() > 4 && match[4].matched ? std::stoi(match.str(4)) : 0}}; } @@ -2754,6 +2757,24 @@ class NetworkTests return GetRoutingTableState(out, defaultRoutePattern, routePattern); } + static void WaitForIpv6DefaultRoute() + { + const auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(30); + while (std::chrono::steady_clock::now() < timeout) + { + auto state = GetIpv6RoutingTableState(); + if (state.DefaultRoute.has_value()) + { + return; + } + + LogInfo("Waiting for IPv6 default route..."); + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + VERIFY_FAIL(L"Timed out waiting for IPv6 default route"); + } + static RoutingTableState GetIpv6RoutingTableState() { auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"ip -6 route show"); @@ -3255,7 +3276,8 @@ class NetworkTests NET_LUID interfaceLuid{}; CONTINUE_IF_FAILED_WIN32(ConvertInterfaceGuidToLuid(&interfaceGuid, &interfaceLuid)); - for (const IP_ADAPTER_ADDRESSES* adapter = adapterAddresses.get(); adapter != nullptr; adapter = adapter->Next) + for (auto* adapter = reinterpret_cast(adapterAddresses.data()); adapter != nullptr; + adapter = adapter->Next) { if (interfaceLuid.Value == adapter->Luid.Value && adapter->FirstUnicastAddress != nullptr && adapter->FirstGatewayAddress != nullptr) { @@ -3267,18 +3289,63 @@ class NetworkTests return false; } - static std::unique_ptr GetAdapterAddresses(ADDRESS_FAMILY family) + static bool HostHasIpv6DnsServers() + { + ULONG bufferSize = 0; + constexpr ULONG flags = GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; + std::vector buffer; + ULONG result = GetAdaptersAddresses(AF_INET6, flags, nullptr, nullptr, &bufferSize); + while (result == ERROR_BUFFER_OVERFLOW) + { + buffer.resize(bufferSize); + result = GetAdaptersAddresses(AF_INET6, flags, nullptr, reinterpret_cast(buffer.data()), &bufferSize); + } + + if (result != NO_ERROR) + { + return false; + } + + DWORD bestIndex = 0; + SOCKADDR_IN6 dest{}; + dest.sin6_family = AF_INET6; + InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr); + + if (GetBestInterfaceEx(reinterpret_cast(&dest), &bestIndex) != NO_ERROR) + { + return false; + } + + for (auto* adapter = reinterpret_cast(buffer.data()); adapter != nullptr; adapter = adapter->Next) + { + if (adapter->IfIndex != bestIndex) + { + continue; + } + + for (auto* dns = adapter->FirstDnsServerAddress; dns != nullptr; dns = dns->Next) + { + if (dns->Address.lpSockaddr->sa_family == AF_INET6) + { + return true; + } + } + } + + return false; + } + + static std::vector GetAdapterAddresses(ADDRESS_FAMILY family) { - ULONG result; constexpr ULONG flags = (GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_INCLUDE_GATEWAYS); ULONG bufferSize = 0; - std::unique_ptr buffer; - - while ((result = GetAdaptersAddresses(family, flags, nullptr, buffer.get(), &bufferSize)) == ERROR_BUFFER_OVERFLOW) + std::vector buffer; + ULONG result = GetAdaptersAddresses(family, flags, nullptr, nullptr, &bufferSize); + while (result == ERROR_BUFFER_OVERFLOW) { - buffer.reset(static_cast(malloc(bufferSize))); - VERIFY_IS_NOT_NULL(buffer.get()); + buffer.resize(bufferSize); + result = GetAdaptersAddresses(family, flags, nullptr, reinterpret_cast(buffer.data()), &bufferSize); } VERIFY_WIN32_SUCCEEDED(result); @@ -4566,6 +4633,8 @@ class VirtioProxyTests return; } + NetworkTests::WaitForIpv6DefaultRoute(); + NetworkTests::GuestClient(L"tcp6-connect:bing.com:80"); } @@ -4584,8 +4653,14 @@ class VirtioProxyTests VERIFY_IS_TRUE(std::regex_match(out, pattern)); - // Verify that /etc/resolv.conf contains a 'search' line with DNS suffixes - VERIFY_IS_TRUE(out.find(L"search ") != std::wstring::npos); + // Verify that /etc/resolv.conf contains a 'search' line if the host has DNS suffixes + auto [suffixOut, suffixErr] = LxsstuLaunchPowershellAndCaptureOutput( + L"(@((Get-DnsClientGlobalSetting).SuffixSearchList) + @((Get-DnsClient).ConnectionSpecificSuffix) | Where-Object " + L"{$_}).Count"); + if (_wtoi(suffixOut.c_str()) > 0) + { + VERIFY_IS_TRUE(out.find(L"search ") != std::wstring::npos); + } } TEST_METHOD(GuestPortIsReleased) @@ -4602,23 +4677,18 @@ class VirtioProxyTests NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false); } - const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); - VERIFY_IS_TRUE(!!listenSocket); + wsl::shared::retry::RetryWithTimeout( + [&]() { + const wil::unique_socket listenSocket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + THROW_HR_IF(E_ABORT, !listenSocket); - SOCKADDR_IN Address{}; - Address.sin_family = AF_INET; - Address.sin_port = htons(1234); - - const auto timeout = std::chrono::steady_clock::now() + std::chrono::minutes(2); - - bool bound = false; - while (!bound && std::chrono::steady_clock::now() < timeout) - { - bound = bind(listenSocket.get(), reinterpret_cast(&Address), sizeof(Address)) != SOCKET_ERROR; - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - - VERIFY_IS_TRUE(bound); + SOCKADDR_IN Address{}; + Address.sin_family = AF_INET; + Address.sin_port = htons(1234); + THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast(&Address), sizeof(Address)) == SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::minutes(2)); } TEST_METHOD(LoopbackGuestToHost) @@ -4690,5 +4760,150 @@ class VirtioProxyTests m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .autoProxy = true})); NetworkTests::VerifyHttpProxySimple(); } + + TEST_METHOD(ConfigurationV6) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); + + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6)) + { + LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); + return; + } + + // Wait for the device host to send an IPv6 Router Advertisement + // before querying the interface state. + NetworkTests::WaitForIpv6DefaultRoute(); + + const auto state = NetworkTests::GetInterfaceState(L"eth0"); + + // Verify that the guest has a global IPv6 address assigned + VERIFY_IS_FALSE(state.V6Addresses.empty()); + + // Verify that the guest has an IPv6 default gateway + VERIFY_IS_TRUE(state.V6Gateway.has_value()); + + // Verify the guest IPv6 address matches the host global IPv6 address + const auto adapterAddresses = NetworkTests::GetAdapterAddresses(AF_INET6); + DWORD bestIndex = 0; + SOCKADDR_IN6 dest{}; + dest.sin6_family = AF_INET6; + InetPtonW(AF_INET6, L"2001:4860:4860::8888", &dest.sin6_addr); + VERIFY_ARE_EQUAL(NO_ERROR, GetBestInterfaceEx(reinterpret_cast(&dest), &bestIndex)); + + for (auto* adapter = reinterpret_cast(adapterAddresses.data()); adapter != nullptr; + adapter = adapter->Next) + { + if (adapter->IfIndex != bestIndex) + { + continue; + } + + for (auto* unicast = adapter->FirstUnicastAddress; unicast != nullptr; unicast = unicast->Next) + { + if (unicast->Address.lpSockaddr->sa_family != AF_INET6) + { + continue; + } + + const auto& sin6 = *reinterpret_cast(unicast->Address.lpSockaddr); + if (IN6_IS_ADDR_LINKLOCAL(&sin6.sin6_addr) || IN6_IS_ADDR_LOOPBACK(&sin6.sin6_addr)) + { + continue; + } + + SOCKADDR_INET hostAddr{}; + hostAddr.Ipv6 = sin6; + const auto hostAddrString = wsl::windows::common::string::SockAddrInetToWstring(hostAddr); + + // The host address may not be at index 0 due to SLAAC addresses from RA + bool addressFound = false; + for (const auto& v6Addr : state.V6Addresses) + { + if (v6Addr.Address == hostAddrString) + { + addressFound = true; + break; + } + } + VERIFY_IS_TRUE(addressFound); + break; + } + + break; + } + } + + TEST_METHOD(DnsResolutionDigV6) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); + + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6)) + { + LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); + return; + } + + // Test AAAA record resolution (IPv6) with both UDP and TCP + NetworkTests::VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com"); + NetworkTests::VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com"); + } + + TEST_METHOD(GuestPortIsReleasedV6) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); + + // Make sure the VM doesn't time out + WslKeepAlive keepAlive; + + { + auto guestProcess = NetworkTests::BindGuestPort(L"TCP6-LISTEN:1234,bind=::", true); + NetworkTests::BindHostPort(1234, SOCK_STREAM, IPPROTO_TCP, false, true); + } + + wsl::shared::retry::RetryWithTimeout( + [&]() { + const wil::unique_socket listenSocket(socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP)); + THROW_HR_IF(E_ABORT, !listenSocket); + + SOCKADDR_IN6 Address{}; + Address.sin6_family = AF_INET6; + Address.sin6_port = htons(1234); + THROW_HR_IF(E_FAIL, bind(listenSocket.get(), reinterpret_cast(&Address), sizeof(Address)) == SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::minutes(2)); + } + + TEST_METHOD(ConfigurationV6DnsServers) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); + + if (!NetworkTests::HostHasInternetConnectivity(AF_INET6)) + { + LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); + return; + } + + if (!NetworkTests::HostHasIpv6DnsServers()) + { + LogSkipped("Host does not have IPv6 DNS servers configured. Skipping..."); + return; + } + + auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"cat /etc/resolv.conf", 0); + + // Verify that /etc/resolv.conf contains at least one IPv6 nameserver + const std::wregex v6Pattern(L"(.|\\n)*nameserver [0-9a-fA-F:]+\\n(.|\\n)*"); + VERIFY_IS_TRUE(std::regex_match(out, v6Pattern)); + } }; } // namespace NetworkTests From ef8e1c8dba101a25d05d6e1a5d94b01bfa1ac395 Mon Sep 17 00:00:00 2001 From: Daman Mulye Date: Fri, 13 Mar 2026 17:00:57 -0700 Subject: [PATCH 08/23] Track `bind` syscall when port is 0 (#14333) * Initial work * . * pr feedback and add unit test * minor tweaks an fix use after free in logging statement * implement PR feedback * hopefully final pr feedback * pr feedback in test function * Address PR feedback: add try/catch to TrackPort and PortZeroBind queue push --------- Co-authored-by: Ben Hillis --- src/linux/init/GnsPortTracker.cpp | 227 +++++++++++++++++++++++++++--- src/linux/init/GnsPortTracker.h | 39 ++++- test/windows/NetworkTests.cpp | 152 ++++++++++++++++++++ 3 files changed, 400 insertions(+), 18 deletions(-) diff --git a/src/linux/init/GnsPortTracker.cpp b/src/linux/init/GnsPortTracker.cpp index d5afaf6fd..669582a14 100644 --- a/src/linux/init/GnsPortTracker.cpp +++ b/src/linux/init/GnsPortTracker.cpp @@ -2,12 +2,12 @@ #include #include -#include #include #include /* Definition of AUDIT_* constants */ #include #include #include +#include #include #include "common.h" // Needs to be included before sal.h before of __reserved macro #include "NetlinkTransactionError.h" @@ -73,6 +73,7 @@ void GnsPortTracker::Run() // for port deallocation std::thread{std::bind(&GnsPortTracker::RunPortRefresh, this)}.detach(); + std::thread{std::bind(&GnsPortTracker::RunDeferredResolve, this)}.detach(); auto future = std::make_optional(m_allocatedPortsRefresh.get_future()); std::optional refreshResult; @@ -98,7 +99,7 @@ void GnsPortTracker::Run() result = HandleRequest(allocationRequest); if (result == 0) { - m_allocatedPorts.emplace(std::make_pair(allocationRequest, std::make_optional(time(nullptr) + c_bind_timeout_seconds))); + TrackPort(allocationRequest); GNS_LOG_INFO( "Tracking bind call: family ({}) port ({}) protocol ({})", allocationRequest.Family, @@ -115,6 +116,20 @@ void GnsPortTracker::Run() { GNS_LOG_ERROR("Failed to complete bind request, {}", e.what()); } + + if (bindCall->PortZeroBind.has_value()) + { + try + { + std::lock_guard lock(m_deferredMutex); + m_deferredQueue.push_back(std::move(bindCall->PortZeroBind.value())); + m_deferredCv.notify_one(); + } + catch (const std::exception& e) + { + GNS_LOG_ERROR("Failed to queue port-0 bind for deferred resolution, {}", e.what()); + } + } } // If bindCall is empty, then the read() timed out. Look for any closed port @@ -133,12 +148,39 @@ void GnsPortTracker::Run() } } + // Process any port-0 binds that the background thread has resolved. + std::deque resolved; + { + std::lock_guard lock(m_resolvedMutex); + resolved.swap(m_resolvedQueue); + } + for (auto& allocation : resolved) + { + const auto result = HandleRequest(allocation); + if (result == 0) + { + TrackPort(std::move(allocation)); + } + else + { + GNS_LOG_ERROR( + "Failed to register resolved port-0 bind: family ({}) port ({}) protocol ({}), error {}", + allocation.Family, + allocation.Port, + allocation.Protocol, + result); + } + } + // Only look at bound ports if there's something to deallocate to avoid wasting cycles - if (refreshResult.has_value() && !m_allocatedPorts.empty()) + if (refreshResult.has_value()) { - future = m_allocatedPortsRefresh.get_future(); - refreshResult->Resume(); // This will resume the sock_diag thread - refreshResult.reset(); + if (!m_allocatedPorts.empty()) + { + future = m_allocatedPortsRefresh.get_future(); + refreshResult->Resume(); // This will resume the sock_diag thread + refreshResult.reset(); + } } } } @@ -227,6 +269,7 @@ void GnsPortTracker::OnRefreshAllocatedPorts(const std::set& Por it->first.Family, it->first.Port, it->first.Protocol); + it = m_allocatedPorts.erase(it); continue; } @@ -299,8 +342,8 @@ std::optional GnsPortTracker::ReadNextRequest() } catch (const std::exception& e) { - GNS_LOG_ERROR("Fetch to read bind() call info with ID {}lu for pid {}, {}", callInfo.id, callInfo.pid, e.what()); - return {{{}, callInfo.id}}; + GNS_LOG_ERROR("Failed to read bind() call info with ID {} for pid {}, {}", callInfo.id, callInfo.pid, e.what()); + return {{{}, {}, callInfo.id}}; } } @@ -310,14 +353,14 @@ std::optional GnsPortTracker::GetCallInfo( auto ParseSocket = [&](int Socket, size_t AddressPtr, size_t AddressLength) -> std::optional { if (AddressLength < sizeof(sockaddr)) { - return {{{}, CallId}}; // Invalid sockaddr. Let it go through. + return {{{}, {}, CallId}}; // Invalid sockaddr. Let it go through. } auto networkNamespace = std::filesystem::read_symlink(std::format("/proc/{}/ns/net", Pid)).string(); if (networkNamespace != m_networkNamespace) { GNS_LOG_INFO("Skipping bind() call for pid {} in network namespace {}", Pid, networkNamespace.c_str()); - return {{{}, CallId}}; // Different network namespace. Let it go through. + return {{{}, {}, CallId}}; // Different network namespace. Let it go through. } auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, AddressPtr, AddressLength); @@ -331,7 +374,7 @@ std::optional GnsPortTracker::GetCallInfo( if ((address.sa_family != AF_INET && address.sa_family != AF_INET6) || (address.sa_family == AF_INET6 && AddressLength < sizeof(sockaddr_in6))) { - return {{{}, CallId}}; // This is a non IP call, or invalid sockaddr_in6. Let it go through + return {{{}, {}, CallId}}; // This is a non IP call, or invalid sockaddr_in6. Let it go through } // Read the port. The port *happens* to be in the same spot in memory for both sockaddr_in @@ -343,7 +386,28 @@ std::optional GnsPortTracker::GetCallInfo( in_port_t port = ntohs(inAddr->sin_port); if (port == 0) { - return {{{}, CallId}}; // If port is 0, just let the call go through + // Port 0 means the kernel will assign an ephemeral port. We can't know + // the port until after the bind() completes, so duplicate the socket fd + // now (while the process is still stopped by seccomp) and defer the + // getsockname() lookup to after CompleteRequest() unblocks it. + try + { + const int protocol = GetSocketProtocol(Pid, Socket); + auto dupFd = DuplicateSocketFd(Pid, Socket); + if (!dupFd) + { + return {{{}, {}, CallId}}; + } + if (!m_seccompDispatcher->ValidateCookie(CallId)) + { + return {{{}, {}, CallId}}; + } + return {{{}, DeferredPortLookup{Pid, std::move(dupFd), protocol}, CallId}}; + } + catch (const std::exception&) + { + return {{{}, {}, CallId}}; // Can't determine protocol, just let it through + } } in6_addr storedAddress = {}; @@ -370,7 +434,7 @@ std::optional GnsPortTracker::GetCallInfo( throw RuntimeErrorWithSourceLocation(std::format("Invalid call id {}", CallId)); } - return {{{PortAllocation(port, address.sa_family, protocol, storedAddress)}, CallId}}; + return {{{PortAllocation(port, address.sa_family, protocol, storedAddress)}, {}, CallId}}; }; #ifdef __x86_64__ if (Arch & __AUDIT_ARCH_64BIT) @@ -384,7 +448,7 @@ std::optional GnsPortTracker::GetCallInfo( { if (Arguments[0] != SYS_BIND) { - return {{{}, CallId}}; // Not a bind call, just let the call go through + return {{{}, {}, CallId}}; // Not a bind call, just let the call go through } // Grab the first 3 parameters auto processMemory = m_seccompDispatcher->ReadProcessMemory(CallId, Pid, Arguments[1], sizeof(uint32_t) * 3); @@ -410,7 +474,7 @@ int GnsPortTracker::GetSocketProtocol(int pid, int fd) { const auto path = std::format("/proc/{}/fd/{}", pid, fd); - // Because there's a race between the time where the buffer size is determined and + // Because there's a race between the time where the buffer size is determined // and the actual getxattr() call, retry until the buffer size is big enough std::string protocol; int result = -1; @@ -424,7 +488,7 @@ int GnsPortTracker::GetSocketProtocol(int pid, int fd) if (result < 0) { - RuntimeErrorWithSourceLocation(std::format("Failed to read protocol for socket: {}, {}", path, errno)); + throw RuntimeErrorWithSourceLocation(std::format("Failed to read protocol for socket: {}, {}", path, errno)); } // In case the size of the attribute shrunk between the two getxattr calls @@ -442,6 +506,137 @@ int GnsPortTracker::GetSocketProtocol(int pid, int fd) throw RuntimeErrorWithSourceLocation(std::format("Unexpected IP socket protocol: {}", protocol)); } +wil::unique_fd GnsPortTracker::DuplicateSocketFd(pid_t Pid, int SocketFd) +{ + // Duplicate the socket fd from the target process into our address space. + // We cannot use open("/proc/pid/fd/N") for sockets because the symlink target + // (socket:[inode]) is not a valid filesystem path. Use pidfd_getfd() instead. + wil::unique_fd pidFd(static_cast(syscall(SYS_pidfd_open, Pid, 0u))); + if (!pidFd) + { + GNS_LOG_INFO("Port-0 bind: pidfd_open failed for pid {} (errno {})", Pid, errno); + return {}; + } + + wil::unique_fd dupFd(static_cast(syscall(SYS_pidfd_getfd, pidFd.get(), SocketFd, 0u))); + if (!dupFd) + { + GNS_LOG_INFO("Port-0 bind: pidfd_getfd failed for pid {} fd {} (errno {})", Pid, SocketFd, errno); + } + + return dupFd; +} + +void GnsPortTracker::TrackPort(PortAllocation allocation) +try +{ + // Use insert_or_assign so the deallocation timeout is refreshed if the same + // port key is already present (emplace would silently keep the old entry). + m_allocatedPorts.insert_or_assign(std::move(allocation), std::make_optional(time(nullptr) + c_bind_timeout_seconds)); +} +catch (const std::exception& e) +{ + GNS_LOG_ERROR("Failed to track port allocation, {}", e.what()); +} + +void GnsPortTracker::RunDeferredResolve() +{ + UtilSetThreadName("GnsPortZero"); + + for (;;) + { + DeferredPortLookup lookup{0, {}, 0}; + { + std::unique_lock lock(m_deferredMutex); + m_deferredCv.wait(lock, [&] { return !m_deferredQueue.empty(); }); + lookup = std::move(m_deferredQueue.front()); + m_deferredQueue.pop_front(); + } + + const auto pid = lookup.Pid; + try + { + ResolvePortZeroBind(std::move(lookup)); + } + catch (const std::exception& e) + { + GNS_LOG_ERROR("Failed to resolve port-0 bind for pid {}, {}", pid, e.what()); + } + } +} + +void GnsPortTracker::ResolvePortZeroBind(DeferredPortLookup lookup) +{ + // The socket fd was already duplicated (via pidfd_getfd) while the target process + // was stopped by seccomp, so it remains valid even if the process has closed or + // reused the original fd number. + + // The bind() syscall is being completed asynchronously on the seccomp dispatcher + // thread after CompleteRequest() unblocks it. Poll getsockname() briefly until + // the kernel assigns a port. + constexpr int maxRetries = 25; + constexpr auto retryDelay = std::chrono::milliseconds(100); + + in_port_t port = 0; + in6_addr address = {}; + int resolvedFamily = 0; + + for (int attempt = 0; attempt < maxRetries; ++attempt) + { + if (attempt > 0) + { + std::this_thread::sleep_for(retryDelay); + } + + sockaddr_storage storage{}; + socklen_t addrLen = sizeof(storage); + if (getsockname(lookup.DuplicatedSocketFd.get(), reinterpret_cast(&storage), &addrLen) != 0) + { + GNS_LOG_ERROR("Port-0 bind: getsockname failed for pid {} (errno {})", lookup.Pid, errno); + return; + } + + resolvedFamily = static_cast(storage.ss_family); + + if (storage.ss_family == AF_INET) + { + const auto* sin = reinterpret_cast(&storage); + port = ntohs(sin->sin_port); + address.s6_addr32[0] = sin->sin_addr.s_addr; + } + else if (storage.ss_family == AF_INET6) + { + const auto* sin6 = reinterpret_cast(&storage); + port = ntohs(sin6->sin6_port); + memcpy(address.s6_addr32, sin6->sin6_addr.s6_addr32, sizeof(address.s6_addr32)); + } + else + { + GNS_LOG_ERROR("Port-0 bind: unexpected address family ({}) for pid {}", resolvedFamily, lookup.Pid); + return; + } + + if (port != 0) + { + break; + } + } + + if (port == 0) + { + GNS_LOG_ERROR("Port-0 bind: kernel did not assign a port for pid {} after retries", lookup.Pid); + return; + } + + PortAllocation allocation(port, resolvedFamily, lookup.Protocol, address); + GNS_LOG_INFO( + "Port-0 bind resolved: family ({}) port ({}) protocol ({}) for pid {}", resolvedFamily, port, lookup.Protocol, lookup.Pid); + { + std::lock_guard lock(m_resolvedMutex); + m_resolvedQueue.push_back(std::move(allocation)); + } +} + std::ostream& operator<<(std::ostream& out, const GnsPortTracker::PortAllocation& entry) { return out << "Port=" << entry.Port << ", Family=" << entry.Family << ", Protocol=" << entry.Protocol; diff --git a/src/linux/init/GnsPortTracker.h b/src/linux/init/GnsPortTracker.h index c6c138c51..a3f2945a1 100644 --- a/src/linux/init/GnsPortTracker.h +++ b/src/linux/init/GnsPortTracker.h @@ -1,7 +1,9 @@ // Copyright (C) Microsoft Corporation. All rights reserved. #pragma once +#include #include +#include #include #include #include @@ -92,9 +94,27 @@ class GnsPortTracker } }; + struct DeferredPortLookup + { + pid_t Pid; + wil::unique_fd DuplicatedSocketFd; // Duplicated via pidfd_getfd while process was stopped + int Protocol; + + DeferredPortLookup(pid_t Pid, wil::unique_fd DuplicatedSocketFd, int Protocol) : + Pid(Pid), DuplicatedSocketFd(std::move(DuplicatedSocketFd)), Protocol(Protocol) + { + } + + DeferredPortLookup(DeferredPortLookup&&) = default; + DeferredPortLookup& operator=(DeferredPortLookup&&) = default; + DeferredPortLookup(const DeferredPortLookup&) = delete; + DeferredPortLookup& operator=(const DeferredPortLookup&) = delete; + }; + struct BindCall { std::optional Request; + std::optional PortZeroBind; std::uint64_t CallId; }; @@ -118,14 +138,20 @@ class GnsPortTracker int RequestPort(const PortAllocation& Port, bool Allocate); - int ClosePort(const PortAllocation& Port); - int HandleRequest(const PortAllocation& Request); void CompleteRequest(uint64_t Id, int Result); static int GetSocketProtocol(int Pid, int Fd); + static wil::unique_fd DuplicateSocketFd(pid_t Pid, int SocketFd); + + void ResolvePortZeroBind(DeferredPortLookup lookup); + + void RunDeferredResolve(); + + void TrackPort(PortAllocation allocation); + std::map> m_allocatedPorts; std::shared_ptr m_hvSocketChannel; NetlinkChannel m_channel; @@ -137,6 +163,15 @@ class GnsPortTracker std::shared_ptr m_seccompDispatcher; std::string m_networkNamespace; + + std::mutex m_deferredMutex; + std::condition_variable m_deferredCv; + std::deque m_deferredQueue; + + // Resolved port-0 allocations posted by the background RunDeferredResolve thread + // for the main Run() loop to process (keeps SocketChannel access single-threaded). + std::mutex m_resolvedMutex; + std::deque m_resolvedQueue; }; std::ostream& operator<<(std::ostream& out, const GnsPortTracker::PortAllocation& portAllocation); diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 12cfd4fbd..0cdf8d470 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -2023,6 +2023,134 @@ class NetworkTests return std::tuple(std::move(process), std::move(read)); } + // Bind port 0 in the guest and return the process handle and the kernel-assigned port. + // Uses socat's -dd output to extract the actual port from the "listening on" line. + static std::tuple BindGuestPortZero(bool Ipv6 = false) + { + auto [stdErrRead, stdErrWrite] = CreateSubprocessPipe(false, true); + const std::wstring protocol = Ipv6 ? L"TCP6-LISTEN:0" : L"TCP4-LISTEN:0"; + const std::wstring wslCmd = L"socat -dd " + protocol + L" STDOUT"; + auto cmd = LxssGenerateWslCommandLine(wslCmd.data()); + + auto process = LxsstuStartProcess(cmd.data(), nullptr, nullptr, stdErrWrite.get()); + stdErrWrite.reset(); + + // Parse the assigned port from socat's debug output. + // socat -dd prints a line like: "... listening on AF=2 0.0.0.0:PORT" + std::string output(512, '\0'); + DWORD writeOffset = 0; + uint16_t assignedPort = 0; + bool found = false; + + while (!found) + { + // Grow the buffer if full to avoid zero-byte reads and infinite loops. + if (writeOffset == output.size()) + { + output.resize(output.size() * 2); + } + + DWORD bytesRead = 0; + if (!ReadFile(stdErrRead.get(), output.data() + writeOffset, static_cast(output.size() - writeOffset), &bytesRead, nullptr)) + { + break; + } + + if (bytesRead == 0) + { + break; + } + + writeOffset += bytesRead; + LogInfo("output %hs", output.c_str()); + std::string_view outputView(output.data(), writeOffset); + auto pos = outputView.find("listening on"); + if (pos != std::string_view::npos) + { + // Limit the search to just the "listening on" line to avoid + // matching colons in subsequent debug lines socat may emit. + auto lineEnd = outputView.find('\n', pos); + auto line = outputView.substr(pos, lineEnd != std::string_view::npos ? lineEnd - pos : std::string_view::npos); + + // Find the last ':' before the port digits. For IPv6, socat outputs + // "listening on AF=10 :::PORT", so using find() would match the + // first colon in the address instead of the port separator. + auto colonPos = line.rfind(':'); + if (colonPos != std::string_view::npos) + { + auto portStr = line.substr(colonPos + 1); + auto end = portStr.find_first_not_of("0123456789"); + if (end != std::string_view::npos) + { + portStr = portStr.substr(0, end); + } + + if (portStr.empty()) + { + continue; + } + + assignedPort = static_cast(std::stoi(std::string(portStr))); + found = true; + } + } + } + + VERIFY_IS_TRUE(found); + VERIFY_IS_TRUE(assignedPort > 0); + LogInfo("Port-0 bind resolved to port %u", assignedPort); + + return {std::move(process), assignedPort}; + } + + static void VerifyPortZeroBindIsTracked(bool verifyRelease = true) + { + // Make sure the VM doesn't time out while we wait for async port resolution + WslKeepAlive keepAlive; + + // Bind port 0 in the guest - the kernel assigns an ephemeral port. + // The port tracker intercepts the bind() via seccomp and defers lookup + // to a background thread that resolves the actual port via getsockname(). + auto [guestProcess, assignedPort] = BindGuestPortZero(); + + // The port-0 resolution is asynchronous (deferred to a background thread). + // Retry until the host port tracker registers the port, blocking the host bind. + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout( + [&assignedPort]() { + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + THROW_LAST_ERROR_IF(!sock); + + SOCKADDR_IN addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(assignedPort); + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast(&addr), sizeof(addr)) != SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::seconds(30))); + + if (!verifyRelease) + { + return; + } + + // Kill the guest process so the port tracker releases the port. + guestProcess.reset(); + + // Retry until the host can bind the port again, confirming it was released. + VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout( + [&assignedPort]() { + wil::unique_socket sock(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)); + THROW_LAST_ERROR_IF(!sock); + + SOCKADDR_IN addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(assignedPort); + THROW_HR_IF(E_FAIL, bind(sock.get(), reinterpret_cast(&addr), sizeof(addr)) == SOCKET_ERROR); + }, + std::chrono::seconds(1), + std::chrono::minutes(2))); + } + template static void VerifyNotBound(T& Address, int AddressFamily, int Protocol) { @@ -3920,6 +4048,21 @@ class MirroredTests auto udpPort = NetworkTests::BindGuestPort(L"UDP4-LISTEN:0", true); } + TEST_METHOD(PortZeroBindIsTracked) + { + MIRRORED_NETWORKING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); + WaitForMirroredStateInLinux(); + + // Skip port-release verification in mirrored mode. The host reserves a contiguous + // ephemeral port range via HcnReserveGuestNetworkServicePortRange that no Windows + // process can bind for the lifetime of the VM. Port-0 binds resolve to ports within + // this range, so even after the guest releases the port the host still cannot bind + // it — the range-level reservation remains, making release unverifiable. + NetworkTests::VerifyPortZeroBindIsTracked(false); + } + TEST_METHOD(ExplicitEphemeralBind) { MIRRORED_NETWORKING_TEST_ONLY(); @@ -4752,6 +4895,15 @@ class VirtioProxyTests NetworkTests::VerifyDnsResolutionRecordTypes(); } + TEST_METHOD(PortZeroBindIsTracked) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); + + NetworkTests::VerifyPortZeroBindIsTracked(); + } + TEST_METHOD(HttpProxySimple) { VIRTIOPROXY_TEST_ONLY(); From 65a8dd660f5e1b3d492fffb02c200e8a11cd2c8a Mon Sep 17 00:00:00 2001 From: Andre Muezerie <108841174+andremueiot@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:48:50 -0400 Subject: [PATCH 09/23] Add iptables to list of apps to install in WSL (#14459) There were instructions already on how to install tcpdump in WSL, but iptables are also needed for the log collection to be complete, so this PR adds instructions on how to also install iptables. Co-authored-by: Andre Muezerie --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c6a33f57..632ba51c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,12 +40,12 @@ Note that WSL distro's launch in the Windows Console (unless you have taken step ### Collect WSL logs for networking issues -Install tcpdump in your WSL distribution using the following commands. +Install iptables and tcpdump in your WSL distribution using the following commands. Note: This will not work if WSL has Internet connectivity issues. ``` # sudo apt-get update -# sudo apt-get -y install tcpdump +# sudo apt-get -y install iptables tcpdump ``` Install [WPR](https://learn.microsoft.com/windows-hardware/test/wpt/windows-performance-recorder) From 20201136f26d2738859c787e8b3ee66ebb54322b Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 17 Mar 2026 12:19:10 -0700 Subject: [PATCH 10/23] Update Microsoft.WSL.DeviceHost to version 1.1.39-0 (#14460) Co-authored-by: Ben Hillis --- packages.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages.config b/packages.config index 8d687ad70..619b94c96 100644 --- a/packages.config +++ b/packages.config @@ -18,7 +18,7 @@ - + From df38f2cc4729880af9a16f5c0b456382888f08fa Mon Sep 17 00:00:00 2001 From: Carlos Nihelton Date: Tue, 17 Mar 2026 20:24:51 -0300 Subject: [PATCH 11/23] Moves all Ubuntu distros to the tar-based format (#14463) * Move all supported Ubuntu images to the new format We backported the build pipeline so all current LTSes come out in the new tar-based format * Remove the appx based distros All WSL users can run tar-based distros by now, right? There is no benefit in maintaining both formats. --- distributions/DistributionInfo.json | 56 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/distributions/DistributionInfo.json b/distributions/DistributionInfo.json index c8e63a679..9a61a24e0 100644 --- a/distributions/DistributionInfo.json +++ b/distributions/DistributionInfo.json @@ -26,6 +26,32 @@ "Url": "https://cdimages.ubuntu.com/releases/24.04.4/release/ubuntu-24.04.4-wsl-arm64.wsl", "Sha256": "6b244d89f412a68f51e58f396fab65bed3b5896a25c045a99bef9c78a07df507" } + }, + { + "Name": "Ubuntu-22.04", + "FriendlyName": "Ubuntu 22.04 LTS", + "Default": false, + "Amd64Url": { + "Url": "https://releases.ubuntu.com/jammy/ubuntu-22.04.5-wsl-amd64.wsl", + "Sha256": "4499c4fe257f2fc83145b429ce211a0a43fd590e70d6261ede616210947d9f8f" + }, + "Arm64Url": { + "Url": "https://cdimage.ubuntu.com/ubuntu/releases/jammy/release/ubuntu-22.04.5-wsl-arm64.wsl", + "Sha256": "3e2d77b92cf9dad1095eaebcad96feba2781007f5f52431cd4fe6fff63b201e5" + } + }, + { + "Name": "Ubuntu-20.04", + "FriendlyName": "Ubuntu 20.04 LTS", + "Default": false, + "Amd64Url": { + "Url": "https://releases.ubuntu.com/focal/ubuntu-20.04.6-wsl-amd64.wsl", + "Sha256": "a9073f3726aab9661076506603706eadf284b80a791dec3adfde8e1989906fa4" + }, + "Arm64Url": { + "Url": "https://cdimage.ubuntu.com/ubuntu/releases/focal/release/ubuntu-20.04.6-wsl-arm64.wsl", + "Sha256": "16174012f9a3f6fb10729e648fd8c7dfa205d039d76902e4c28de746f85bf3c8" + } } ], "openSUSE": [ @@ -243,36 +269,6 @@ "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/KaliLinux_1.13.1.0.AppxBundle", "PackageFamilyName": "KaliLinux.54290C8133FEE_ey8k8hqnwqnmg" }, - { - "Name": "Ubuntu-20.04", - "FriendlyName": "Ubuntu 20.04 LTS", - "StoreAppId": "9MTTCL66CPXJ", - "Amd64": true, - "Arm64": true, - "Amd64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2004-230608_x64.appx", - "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2004-230608_ARM64.appx", - "PackageFamilyName": "CanonicalGroupLimited.Ubuntu20.04LTS_79rhkp1fndgsc" - }, - { - "Name": "Ubuntu-22.04", - "FriendlyName": "Ubuntu 22.04 LTS", - "StoreAppId": "9PN20MSR04DW", - "Amd64": true, - "Arm64": true, - "Amd64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2204LTS-230518_x64.appx", - "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2204LTS-230518_ARM64.appx", - "PackageFamilyName": "CanonicalGroupLimited.Ubuntu22.04LTS_79rhkp1fndgsc" - }, - { - "Name": "Ubuntu-24.04", - "FriendlyName": "Ubuntu 24.04 LTS", - "StoreAppId": "9NZ3KLHXDJP5", - "Amd64": true, - "Arm64": true, - "Amd64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2404-240425.AppxBundle", - "Arm64PackageUrl": "https://publicwsldistros.blob.core.windows.net/wsldistrostorage/Ubuntu2404-240425.AppxBundle", - "PackageFamilyName": "CanonicalGroupLimited.Ubuntu24.04LTS_79rhkp1fndgsc" - }, { "Name": "OracleLinux_7_9", "FriendlyName": "Oracle Linux 7.9", From c5fb4aa6394f16cdb7d002f44fde789a1618fb7e Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 18 Mar 2026 15:31:04 -0700 Subject: [PATCH 12/23] Enable DNS tunneling for VirtioProxy networking mode (#14461) - Allow VirtioProxy to keep EnableDnsTunneling=true in config, but clear socket-specific options (BestEffortDnsParsing, DnsTunnelingIpAddress) - Suppress dedicated DNS tunneling hvsocket for VirtioProxy; tunneling is handled through the VirtioNetworking device host instead - Set DnsTunneling flag on VirtioNetworkingFlags so the device host knows to tunnel DNS - Expand SWIOTLB kernel cmdline to cover VirtioFs and VirtioProxy - Bump DeviceHost package to 1.1.39-0 - Add VirtioProxy DNS test coverage for tunneling on/off - Skip GuestPortIsReleasedV6 on Windows 10 Co-authored-by: Ben Hillis --- src/windows/common/WslCoreConfig.cpp | 14 ++- src/windows/service/exe/WslCoreVm.cpp | 9 +- test/windows/NetworkTests.cpp | 126 +++++++++++++++++--------- 3 files changed, 96 insertions(+), 53 deletions(-) diff --git a/src/windows/common/WslCoreConfig.cpp b/src/windows/common/WslCoreConfig.cpp index ee8b780aa..26c1a4b40 100644 --- a/src/windows/common/WslCoreConfig.cpp +++ b/src/windows/common/WslCoreConfig.cpp @@ -474,15 +474,19 @@ void wsl::core::Config::Initialize(_In_opt_ HANDLE UserToken) EnableVirtio9p = false; } - if (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored) + if (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored && NetworkingMode != NetworkingMode::VirtioProxy) { - VALIDATE_CONFIG_OPTION((NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored), EnableDnsTunneling, false); + VALIDATE_CONFIG_OPTION( + (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored && NetworkingMode != NetworkingMode::VirtioProxy), + EnableDnsTunneling, + false); } - if (!EnableDnsTunneling) + if (!EnableDnsTunneling || NetworkingMode == NetworkingMode::VirtioProxy) { - VALIDATE_CONFIG_OPTION(!EnableDnsTunneling, BestEffortDnsParsing, false); - VALIDATE_CONFIG_OPTION(!EnableDnsTunneling, DnsTunnelingIpAddress, std::optional{}); + VALIDATE_CONFIG_OPTION(!EnableDnsTunneling || NetworkingMode == NetworkingMode::VirtioProxy, BestEffortDnsParsing, false); + VALIDATE_CONFIG_OPTION( + !EnableDnsTunneling || NetworkingMode == NetworkingMode::VirtioProxy, DnsTunnelingIpAddress, std::optional{}); } if (NetworkingMode != NetworkingMode::Mirrored) diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index 952ff7f34..adfc44a1f 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -520,7 +520,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken message->MemoryReclaimMode = static_cast(m_vmConfig.MemoryReclaim); message->EnableDebugShell = m_vmConfig.EnableDebugShell; message->EnableSafeMode = m_vmConfig.EnableSafeMode; - message->EnableDnsTunneling = m_vmConfig.EnableDnsTunneling; + message->EnableDnsTunneling = m_vmConfig.EnableDnsTunneling && m_vmConfig.NetworkingMode != NetworkingMode::VirtioProxy; message->DefaultKernel = m_defaultKernel; message->KernelModulesDeviceId = m_kernelModulesDeviceId; message.WriteString(message->HostnameOffset, wsl::windows::common::filesystem::GetLinuxHostName()); @@ -537,7 +537,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken // Create hvsocket connection for DNS tunneling if enabled. wil::unique_socket dnsTunnelingSocket; - if (m_vmConfig.EnableDnsTunneling) + if (message->EnableDnsTunneling) { dnsTunnelingSocket = AcceptConnection(m_vmConfig.KernelBootTimeout); } @@ -578,6 +578,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken { wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6; WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay, m_vmConfig.EnableLocalhostRelay); + WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::DnsTunneling, m_vmConfig.EnableDnsTunneling); m_networkingEngine = std::make_unique( std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken); } @@ -1555,8 +1556,8 @@ std::wstring WslCoreVm::GenerateConfigJson() // Enable timesync workaround to sync on resume from sleep in modern standby. kernelCmdLine += L" hv_utils.timesync_implicit=1"; - // If using virtio-9p, enable SWIOTLB as a perf optimization (will cause VM to consume 64MB more memory). - if (m_vmConfig.EnableVirtio9p) + // If using virtio features, enable SWIOTLB as a perf optimization (will cause VM to consume 64MB more memory). + if (m_vmConfig.EnableVirtio9p || m_vmConfig.EnableVirtioFs || m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy) { kernelCmdLine += L" swiotlb=force"; } diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 0cdf8d470..f60bfb06b 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -809,6 +809,19 @@ class NetworkTests VerifyDigDnsResolution(L"dig +short +time=5 SOA bing.com"); } + static void VerifyDnsResolutionDigV6() + { + if (!HostHasInternetConnectivity(AF_INET6)) + { + LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); + return; + } + + // Test AAAA record resolution (IPv6) with both UDP and TCP + VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com"); + VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com"); + } + static void VerifyDnsQueries() { // query for A/IPv4 records @@ -4868,33 +4881,6 @@ class VirtioProxyTests auto tcpPort = NetworkTests::BindGuestPort(L"TCP4-LISTEN:2345", true); } - TEST_METHOD(DnsResolutionBasic) - { - VIRTIOPROXY_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); - - NetworkTests::VerifyDnsResolutionBasic(); - } - - TEST_METHOD(DnsResolutionDig) - { - VIRTIOPROXY_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); - - NetworkTests::VerifyDnsResolutionDig(); - } - - TEST_METHOD(DnsResolutionRecordTypes) - { - VIRTIOPROXY_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); - - NetworkTests::VerifyDnsResolutionRecordTypes(); - } - TEST_METHOD(PortZeroBindIsTracked) { VIRTIOPROXY_TEST_ONLY(); @@ -4988,26 +4974,10 @@ class VirtioProxyTests } } - TEST_METHOD(DnsResolutionDigV6) - { - VIRTIOPROXY_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); - - if (!NetworkTests::HostHasInternetConnectivity(AF_INET6)) - { - LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); - return; - } - - // Test AAAA record resolution (IPv6) with both UDP and TCP - NetworkTests::VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com"); - NetworkTests::VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com"); - } - TEST_METHOD(GuestPortIsReleasedV6) { VIRTIOPROXY_TEST_ONLY(); + WINDOWS_11_TEST_ONLY(); m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy})); @@ -5057,5 +5027,73 @@ class VirtioProxyTests const std::wregex v6Pattern(L"(.|\\n)*nameserver [0-9a-fA-F:]+\\n(.|\\n)*"); VERIFY_IS_TRUE(std::regex_match(out, v6Pattern)); } + + TEST_METHOD(DnsResolutionBasicDnsTunneling) + { + VIRTIOPROXY_TEST_ONLY(); + DNS_TUNNELING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); + NetworkTests::VerifyDnsResolutionBasic(); + } + + TEST_METHOD(DnsResolutionDigDnsTunneling) + { + VIRTIOPROXY_TEST_ONLY(); + DNS_TUNNELING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); + NetworkTests::VerifyDnsResolutionDig(); + } + + TEST_METHOD(DnsResolutionRecordTypesDnsTunneling) + { + VIRTIOPROXY_TEST_ONLY(); + DNS_TUNNELING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); + NetworkTests::VerifyDnsResolutionRecordTypes(); + } + + TEST_METHOD(DnsResolutionDigV6DnsTunneling) + { + VIRTIOPROXY_TEST_ONLY(); + DNS_TUNNELING_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); + NetworkTests::VerifyDnsResolutionDigV6(); + } + + TEST_METHOD(DnsResolutionBasic) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); + NetworkTests::VerifyDnsResolutionBasic(); + } + + TEST_METHOD(DnsResolutionDig) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); + NetworkTests::VerifyDnsResolutionDig(); + } + + TEST_METHOD(DnsResolutionRecordTypes) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); + NetworkTests::VerifyDnsResolutionRecordTypes(); + } + + TEST_METHOD(DnsResolutionDigV6) + { + VIRTIOPROXY_TEST_ONLY(); + + m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); + NetworkTests::VerifyDnsResolutionDigV6(); + } }; } // namespace NetworkTests From 6425aa8acc3f18515224a873270b3fb0596df8d4 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 18 Mar 2026 22:45:56 -0700 Subject: [PATCH 13/23] test: disable LoopbackExplicit due to OS build 29555 regression (#14477) Co-authored-by: Ben Hillis --- test/windows/NetworkTests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index f60bfb06b..31135565b 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -3891,6 +3891,9 @@ class MirroredTests TEST_METHOD(LoopbackExplicit) { + // TODO: re-enable once OS build 29555 loopback regression is resolved. + SKIP_TEST_UNSTABLE(); + MIRRORED_NETWORKING_TEST_ONLY(); m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::Mirrored})); From d7ff5b96315a6cd710d1044702182db99a1e2cb9 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 19 Mar 2026 12:45:58 -0700 Subject: [PATCH 14/23] Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES (#14426) * Refactor: trim unnecessary DLL deps from COMMON_LINK_LIBRARIES - Split MSI/Wintrust install functions from wslutil.cpp into install.cpp - Remove MI.lib, wsldeps.lib, msi.lib, Wintrust.lib, computecore.lib, computenetwork.lib, Iphlpapi.lib from COMMON_LINK_LIBRARIES - Add per-target MSI_LINK_LIBRARIES, HCS_LINK_LIBRARIES, SERVICE_LINK_LIBRARIES - Delay-load msi.dll and WINTRUST.dll for wsl.exe and wslg.exe - Result: wslhost, wslrelay, wslcsdk, testplugin lose msi/wintrust startup imports; wsl.exe and wslg.exe defer msi/wintrust loading until actually needed; wslservice is the only target that imports computecore/computenetwork/Iphlpapi * minor fixes to install.cpp that were caught during PR * move to wsl::windows::common::install namespace --------- Co-authored-by: Ben Hillis --- CMakeLists.txt | 14 +- src/windows/common/CMakeLists.txt | 1 + src/windows/common/WslClient.cpp | 11 +- src/windows/common/install.cpp | 455 ++++++++++++++++++ src/windows/common/install.h | 34 ++ src/windows/common/wslutil.cpp | 402 ---------------- src/windows/common/wslutil.h | 14 - src/windows/service/exe/CMakeLists.txt | 4 +- src/windows/service/exe/PluginManager.cpp | 3 +- src/windows/wsl/CMakeLists.txt | 5 +- src/windows/wslg/CMakeLists.txt | 5 +- src/windows/wslinstall/CMakeLists.txt | 2 +- src/windows/wslinstall/DllMain.cpp | 2 + src/windows/wslinstaller/exe/CMakeLists.txt | 1 + src/windows/wslinstaller/exe/WslInstaller.cpp | 5 +- test/windows/CMakeLists.txt | 3 + test/windows/UnitTests.cpp | 3 +- 17 files changed, 531 insertions(+), 433 deletions(-) create mode 100644 src/windows/common/install.cpp create mode 100644 src/windows/common/install.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c2380400..3a1701b70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -250,13 +250,19 @@ set(COMMON_LINK_LIBRARIES Shlwapi.lib synchronization.lib Bcrypt.lib - Iphlpapi.lib - icu.lib + icu.lib) + +set(MSI_LINK_LIBRARIES + Wintrust.lib + msi.lib) + +set(HCS_LINK_LIBRARIES computecore.lib computenetwork.lib + Iphlpapi.lib) + +set(SERVICE_LINK_LIBRARIES MI.lib - Wintrust.lib - msi.lib wsldeps.lib) # Linux diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index d3cdeeb82..6dbe4c0b1 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -45,6 +45,7 @@ set(SOURCES WslTelemetry.cpp VirtioNetworking.cpp wslutil.cpp + install.cpp notifications.cpp) set(HEADERS diff --git a/src/windows/common/WslClient.cpp b/src/windows/common/WslClient.cpp index 72daeecae..49cdb5ef7 100644 --- a/src/windows/common/WslClient.cpp +++ b/src/windows/common/WslClient.cpp @@ -13,6 +13,7 @@ Module Name: --*/ #include "precomp.h" +#include "install.h" #include "WslInstall.h" #include "HandleConsoleProgressBar.h" #include "Distribution.h" @@ -156,7 +157,7 @@ int BashMain(_In_ std::wstring_view commandLine) // Call the MSI package if we're in an MSIX context if (wsl::windows::common::wslutil::IsRunningInMsix()) { - return wsl::windows::common::wslutil::CallMsiPackage(); + return wsl::windows::common::install::CallMsiPackage(); } const auto options = ParseLegacyArguments(commandLine); @@ -1265,7 +1266,7 @@ int UpdatePackage(std::wstring_view commandLine) parser.AddArgument(NoOp(), WSL_UPDATE_ARG_PROMPT_OPTION_LONG); parser.Parse(); - return wsl::windows::common::wslutil::UpdatePackage(preRelease, false); + return wsl::windows::common::install::UpdatePackage(preRelease, false); } int Uninstall() @@ -1274,7 +1275,7 @@ int Uninstall() auto clearLogs = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFile]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFile.c_str())); }); - const auto exitCode = wsl::windows::common::wslutil::UninstallViaMsi(logFile.c_str(), &wsl::windows::common::wslutil::MsiMessageCallback); + const auto exitCode = wsl::windows::common::install::UninstallViaMsi(logFile.c_str(), &wsl::windows::common::install::MsiMessageCallback); if (exitCode != 0) { @@ -1311,7 +1312,7 @@ int WslconfigMain(_In_ int argc, _In_reads_(argc) LPWSTR* argv) // Call the MSI package if we're in an MSIX context if (wsl::windows::common::wslutil::IsRunningInMsix()) { - return wsl::windows::common::wslutil::CallMsiPackage(); + return wsl::windows::common::install::CallMsiPackage(); } using wsl::shared::string::IsEqual; @@ -1524,7 +1525,7 @@ int WslMain(_In_ std::wstring_view commandLine) // Call the MSI package if we're in an MSIX context if (wsl::windows::common::wslutil::IsRunningInMsix()) { - return wsl::windows::common::wslutil::CallMsiPackage(); + return wsl::windows::common::install::CallMsiPackage(); } // Use exit code -1 so invokers of wsl.exe can distinguish between a Linux diff --git a/src/windows/common/install.cpp b/src/windows/common/install.cpp new file mode 100644 index 000000000..740e382d2 --- /dev/null +++ b/src/windows/common/install.cpp @@ -0,0 +1,455 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + install.cpp + +Abstract: + + This file contains MSI/Wintrust install helper functions. + Split from wslutil.cpp to avoid pulling msi.dll/wintrust.dll + into targets that don't need them. + +--*/ + +#include "precomp.h" +#include "install.h" +#include "wslutil.h" +#include "WslPluginApi.h" +#include "wslinstallerservice.h" + +#include "ConsoleProgressBar.h" +#include "ExecutionContext.h" +#include "MsiQuery.h" + +using winrt::Windows::Foundation::Uri; +using winrt::Windows::Management::Deployment::DeploymentOptions; +using wsl::shared::Localization; +using wsl::windows::common::Context; +using namespace wsl::windows::common::registry; +using namespace wsl::windows::common::wslutil; +using namespace wsl::windows::common::install; + +namespace { + +bool PromptForKeyPress() +{ + THROW_IF_WIN32_BOOL_FALSE(FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE))); + + // Note: Ctrl-c causes _getch to return 0x3. + return _getch() != 0x3; +} + +bool PromptForKeyPressWithTimeout() +{ + // Run PromptForKeyPress on a separate thread so we can apply a timeout. + // If PromptForKeyPress fails, fulfill the promise with false so the caller doesn't hang. + std::promise pressedKey; + auto thread = std::thread([&pressedKey]() { + try + { + pressedKey.set_value(PromptForKeyPress()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + try + { + pressedKey.set_value(false); + } + CATCH_LOG() + } + }); + + auto cancelRead = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&thread]() { + if (thread.joinable()) + { + LOG_IF_WIN32_BOOL_FALSE(CancelSynchronousIo(thread.native_handle())); + thread.join(); + } + }); + + auto future = pressedKey.get_future(); + const auto waitResult = future.wait_for(std::chrono::minutes(1)); + + return waitResult == std::future_status::ready && future.get(); +} + +int UpdatePackageImpl(bool preRelease, bool repair) +{ + if (!repair) + { + PrintMessage(Localization::MessageCheckingForUpdates()); + } + + auto [version, release] = GetLatestGitHubRelease(preRelease); + + if (!repair && ParseWslPackageVersion(version) <= wsl::shared::PackageVersion) + { + PrintMessage(Localization::MessageUpdateNotNeeded()); + return 0; + } + + PrintMessage(Localization::MessageUpdatingToVersion(version.c_str())); + + const bool msiInstall = wsl::shared::string::EndsWith(release.name, L".msi"); + const auto downloadPath = DownloadFile(release.url, release.name); + if (msiInstall) + { + auto logFile = std::filesystem::temp_directory_path() / L"wsl-install-logs.txt"; + auto clearLogs = + wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFile]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFile.c_str())); }); + + const auto exitCode = UpgradeViaMsi(downloadPath.c_str(), L"", logFile.c_str(), &MsiMessageCallback); + + if (exitCode != 0) + { + clearLogs.release(); + THROW_HR_WITH_USER_ERROR( + HRESULT_FROM_WIN32(exitCode), + wsl::shared::Localization::MessageUpdateFailed(exitCode) + L"\r\n" + + wsl::shared::Localization::MessageSeeLogFile(logFile.c_str())); + } + } + else + { + // Set FILE_FLAG_DELETE_ON_CLOSE on the file to make sure it's deleted when the installation completes. + const wil::unique_hfile package{CreateFileW( + downloadPath.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, nullptr)}; + + THROW_LAST_ERROR_IF(!package); + + const winrt::Windows::Management::Deployment::PackageManager packageManager; + const auto result = packageManager.AddPackageAsync( + Uri{downloadPath.c_str()}, nullptr, DeploymentOptions::ForceApplicationShutdown | DeploymentOptions::ForceTargetApplicationShutdown); + + THROW_IF_FAILED(result.get().ExtendedErrorCode()); + + // Note: If the installation is successful, this process is expected to receive and Ctrl-C and exit + } + + return 0; +} + +void WaitForMsiInstall() +{ + wil::com_ptr_t installer; + + auto retry_pred = []() { + const auto errorCode = wil::ResultFromCaughtException(); + return errorCode == REGDB_E_CLASSNOTREG; + }; + + wsl::shared::retry::RetryWithTimeout( + [&installer]() { installer = wil::CoCreateInstance(__uuidof(WslInstaller), CLSCTX_LOCAL_SERVER); }, + std::chrono::seconds(1), + std::chrono::minutes(1), + retry_pred); + + fputws(wsl::shared::Localization::MessageFinishMsiInstallation().c_str(), stderr); + + auto finishLine = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { fputws(L"\n", stderr); }); + + UINT exitCode = -1; + wil::unique_cotaskmem_string message{}; + THROW_IF_FAILED(installer->Install(&exitCode, &message)); + + if (message && *message.get() != UNICODE_NULL) + { + finishLine.release(); + wprintf(L"\n%ls\n", message.get()); + } + + if (exitCode != 0) + { + THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(exitCode), wsl::shared::Localization::MessageUpdateFailed(exitCode)); + } +} + +wil::unique_handle CreateJob() +{ + // Create a job object that will terminate all processes in the job on + // close but will not terminate the children of the processes in the job. + // This is used to ensure that when forwarding from an inbox binary (I) + // to a lifted binary (L), if I is terminated L is terminated as well but + // any children of L (e.g. wslhost.exe) continue to run. + wil::unique_handle job{CreateJobObject(nullptr, nullptr)}; + THROW_LAST_ERROR_IF_NULL(job.get()); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; + info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; + THROW_IF_WIN32_BOOL_FALSE(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &info, sizeof(info))); + + return job; +} + +int WINAPI InstallRecordHandler(void* context, UINT messageType, LPCWSTR message) +{ + try + { + WSL_LOG("MSIMessage", TraceLoggingValue(messageType, "type"), TraceLoggingValue(message, "message")); + auto type = (INSTALLMESSAGE)(0xFF000000 & (UINT)messageType); + + if (type == INSTALLMESSAGE_ERROR || type == INSTALLMESSAGE_FATALEXIT || type == INSTALLMESSAGE_WARNING) + { + WriteInstallLog(std::format("MSI message: {}", message)); + } + + auto* callback = reinterpret_cast*>(context); + if (callback != nullptr) + { + (*callback)(type, message); + } + } + CATCH_LOG(); + + return IDOK; +} + +void ConfigureMsiLogging(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) +{ + if (LogFile != nullptr) + { + LOG_IF_WIN32_ERROR(MsiEnableLog(INSTALLLOGMODE_VERBOSE | INSTALLLOGMODE_EXTRADEBUG | INSTALLLOGMODE_PROGRESS, LogFile, 0)); + } + + MsiSetExternalUI( + &InstallRecordHandler, + INSTALLLOGMODE_FATALEXIT | INSTALLLOGMODE_ERROR | INSTALLLOGMODE_WARNING | INSTALLLOGMODE_USER | INSTALLLOGMODE_INFO | + INSTALLLOGMODE_RESOLVESOURCE | INSTALLLOGMODE_OUTOFDISKSPACE | INSTALLLOGMODE_ACTIONSTART | INSTALLLOGMODE_ACTIONDATA | + INSTALLLOGMODE_COMMONDATA | INSTALLLOGMODE_INITIALIZE | INSTALLLOGMODE_TERMINATE | INSTALLLOGMODE_SHOWDIALOG, + (void*)&Callback); + + MsiSetInternalUI(INSTALLUILEVEL(INSTALLUILEVEL_NONE | INSTALLUILEVEL_UACONLY | INSTALLUILEVEL_SOURCERESONLY), nullptr); +} + +} // namespace + +int wsl::windows::common::install::CallMsiPackage() +{ + wsl::windows::common::ExecutionContext context(wsl::windows::common::CallMsi); + + auto msiPath = GetMsiPackagePath(); + if (!msiPath.has_value()) + { + wsl::windows::common::ExecutionContext context(wsl::windows::common::Install); + + try + { + WaitForMsiInstall(); + msiPath = GetMsiPackagePath(); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + + // GetMsiPackagePath() will generate a user error if the registry access fails. + // Save the error from GetMsiPackagePath() to return a proper 'install failed' message. + auto savedError = context.ReportedError(); + + // There is a race where the service might stop before returning the install result. + // if this happens, only fail if the MSI still isn't installed. + msiPath = GetMsiPackagePath(); + if (!msiPath.has_value()) + { + // Offer to directly install the MSI package if the MsixInstaller logic fails + // This can trigger a UAC so only do it + if (IsInteractiveConsole()) + { + auto errorCode = savedError.has_value() ? ErrorToString(savedError.value()).Code + : ErrorCodeToString(wil::ResultFromCaughtException()); + + EMIT_USER_WARNING(wsl::shared::Localization::MessageInstallationCorrupted(errorCode)); + + if (PromptForKeyPressWithTimeout()) + { + return UpdatePackage(false, true); + } + } + + if (savedError.has_value()) + { + THROW_HR_WITH_USER_ERROR(savedError->Code, savedError->Message.value_or(L"")); + } + + throw; + } + } + + THROW_HR_IF(E_UNEXPECTED, !msiPath.has_value()); + } + + auto target = msiPath.value() + L"\\" WSL_BINARY_NAME; + + SubProcess process(target.c_str(), GetCommandLine()); + process.SetDesktopAppPolicy(PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE); + auto runningProcess = process.Start(); + + // N.B. The job cannot be assigned at process creation time as the packaged process + // creation path will assign the new process to a per package job object. + // In the case of multiple processes running in a single package, assigning + // the new process to the per package job object will fail for the second request + // since both jobs already have processes which prevents a job hierarchy from + // being established. + auto job = CreateJob(); + + // Assign the process to the job, ignoring failures when the process has + // terminated. + // + // N.B. Assigning the job after process creation without CREATE_SUSPENDED is + // safe to do here since only the new child process will be in the job + // object. None of the grandchildren processes are included since the + // job is created with JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK. + if (!AssignProcessToJobObject(job.get(), runningProcess.get())) + { + auto lastError = GetLastError(); + if (lastError != ERROR_ACCESS_DENIED) + { + THROW_WIN32(lastError); + } + } + + return static_cast(SubProcess::GetExitCode(runningProcess.get())); +} + +void wsl::windows::common::install::MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message) +{ + switch (type) + { + case INSTALLMESSAGE_ERROR: + case INSTALLMESSAGE_FATALEXIT: + case INSTALLMESSAGE_WARNING: + wprintf(L"%ls\n", message); + break; + + default: + break; + } +} + +int wsl::windows::common::install::UpdatePackage(bool PreRelease, bool Repair) +{ + // Register a console control handler so "^C" is not printed when the app platform terminates the process. + THROW_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler( + [](DWORD ctrlType) { + if (ctrlType == CTRL_C_EVENT) + { + ExitProcess(0); + } + return FALSE; + }, + TRUE)); + + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { SetConsoleCtrlHandler(nullptr, FALSE); }); + + try + { + return UpdatePackageImpl(PreRelease, Repair); + } + catch (...) + { + // Rethrowing via WIL is required for the error context to be properly set in case a winrt exception was thrown. + THROW_HR(wil::ResultFromCaughtException()); + } +} + +UINT wsl::windows::common::install::UpgradeViaMsi( + _In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) +{ + WriteInstallLog(std::format("Upgrading via MSI package: {}. Args: {}", PackageLocation, ExtraArgs != nullptr ? ExtraArgs : L"")); + + ConfigureMsiLogging(LogFile, Callback); + + auto result = MsiInstallProduct(PackageLocation, ExtraArgs); + WSL_LOG( + "MsiInstallResult", + TraceLoggingValue(result, "result"), + TraceLoggingValue(ExtraArgs != nullptr ? ExtraArgs : L"", "ExtraArgs")); + + WriteInstallLog(std::format("MSI upgrade result: {}", result)); + + return result; +} + +UINT wsl::windows::common::install::UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) +{ + const auto key = OpenLxssMachineKey(KEY_READ); + const auto productCode = ReadString(key.get(), L"Msi", L"ProductCode", nullptr); + + WriteInstallLog(std::format("Uninstalling MSI package: {}", productCode)); + + ConfigureMsiLogging(LogFile, Callback); + + auto result = MsiConfigureProduct(productCode.c_str(), 0, INSTALLSTATE_ABSENT); + WSL_LOG("MsiUninstallResult", TraceLoggingValue(result, "result")); + + WriteInstallLog(std::format("MSI package uninstall result: {}", result)); + + return result; +} + +wil::unique_hfile wsl::windows::common::install::ValidateFileSignature(LPCWSTR Path) +{ + wil::unique_hfile fileHandle{CreateFileW(Path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)}; + THROW_LAST_ERROR_IF(!fileHandle); + + GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; + WINTRUST_DATA trust{}; + trust.cbStruct = sizeof(trust); + trust.dwUIChoice = WTD_UI_NONE; + trust.dwUnionChoice = WTD_CHOICE_FILE; + trust.dwStateAction = WTD_STATEACTION_VERIFY; + + WINTRUST_FILE_INFO file = {0}; + file.cbStruct = sizeof(file); + file.hFile = fileHandle.get(); + trust.pFile = &file; + + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + trust.dwStateAction = WTD_STATEACTION_CLOSE; + WinVerifyTrust(nullptr, &action, &trust); + }); + + THROW_IF_WIN32_ERROR(WinVerifyTrust(nullptr, &action, &trust)); + + return fileHandle; +} + +void wsl::windows::common::install::WriteInstallLog(const std::string& Content) +try +{ + static std::wstring path = wil::GetWindowsDirectoryW() + L"\\temp\\wsl-install-log.txt"; + + // Wait up to 10 seconds for the log file mutex + wil::unique_handle mutex{CreateMutex(nullptr, true, L"Global\\WslInstallLog")}; + THROW_LAST_ERROR_IF(!mutex); + + THROW_LAST_ERROR_IF(WaitForSingleObject(mutex.get(), 10 * 1000) != WAIT_OBJECT_0); + + wil::unique_handle file{CreateFile( + path.c_str(), GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, 0, nullptr)}; + + THROW_LAST_ERROR_IF(!file); + + LARGE_INTEGER size{}; + THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(file.get(), &size)); + + // Append to the file if its size is below 10MB, otherwise truncate. + if (size.QuadPart < 10 * _1MB) + { + THROW_LAST_ERROR_IF(SetFilePointer(file.get(), 0, nullptr, FILE_END) == INVALID_SET_FILE_POINTER); + } + else + { + THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get())); + } + + static auto processName = wil::GetModuleFileNameW(); + auto logLine = std::format("{:%FT%TZ} {}[{}]: {}\n", std::chrono::system_clock::now(), processName, WSL_PACKAGE_VERSION, Content); + + DWORD bytesWritten{}; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), logLine.c_str(), static_cast(logLine.size()), &bytesWritten, nullptr)); +} +CATCH_LOG(); diff --git a/src/windows/common/install.h b/src/windows/common/install.h new file mode 100644 index 000000000..fae741e04 --- /dev/null +++ b/src/windows/common/install.h @@ -0,0 +1,34 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + install.h + +Abstract: + + This file contains MSI/Wintrust install helper function declarations. + +--*/ + +#pragma once +#include + +namespace wsl::windows::common::install { + +int CallMsiPackage(); + +void MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message); + +wil::unique_hfile ValidateFileSignature(LPCWSTR Path); + +int UpdatePackage(bool PreRelease, bool Repair); + +UINT UpgradeViaMsi(_In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); + +UINT UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); + +void WriteInstallLog(const std::string& Content); + +} // namespace wsl::windows::common::install diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 421caf89e..24d21733e 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -216,141 +216,6 @@ bool IsWinInetError(HRESULT error) return code >= INTERNET_ERROR_BASE && code <= INTERNET_ERROR_LAST; } -bool PromptForKeyPress() -{ - THROW_IF_WIN32_BOOL_FALSE(FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE))); - - // Note: Ctrl-c causes _getch to return 0x3. - return _getch() != 0x3; -} - -bool PromptForKeyPressWithTimeout() -{ - std::promise pressedKey; - auto thread = std::thread([&pressedKey]() { pressedKey.set_value(PromptForKeyPress()); }); - - auto cancelRead = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&thread]() { - if (thread.joinable()) - { - LOG_IF_WIN32_BOOL_FALSE(CancelSynchronousIo(thread.native_handle())); - thread.join(); - } - }); - - auto future = pressedKey.get_future(); - const auto waitResult = future.wait_for(std::chrono::minutes(1)); - - return waitResult == std::future_status::ready && future.get(); -} - -int UpdatePackageImpl(bool preRelease, bool repair) -{ - if (!repair) - { - PrintMessage(Localization::MessageCheckingForUpdates()); - } - - auto [version, release] = GetLatestGitHubRelease(preRelease); - - if (!repair && ParseWslPackageVersion(version) <= wsl::shared::PackageVersion) - { - PrintMessage(Localization::MessageUpdateNotNeeded()); - return 0; - } - - PrintMessage(Localization::MessageUpdatingToVersion(version.c_str())); - - const bool msiInstall = wsl::shared::string::EndsWith(release.name, L".msi"); - const auto downloadPath = DownloadFile(release.url, release.name); - if (msiInstall) - { - auto logFile = std::filesystem::temp_directory_path() / L"wsl-install-logs.txt"; - auto clearLogs = - wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFile]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFile.c_str())); }); - - const auto exitCode = UpgradeViaMsi(downloadPath.c_str(), L"", logFile.c_str(), &MsiMessageCallback); - - if (exitCode != 0) - { - clearLogs.release(); - THROW_HR_WITH_USER_ERROR( - HRESULT_FROM_WIN32(exitCode), - wsl::shared::Localization::MessageUpdateFailed(exitCode) + L"\r\n" + - wsl::shared::Localization::MessageSeeLogFile(logFile.c_str())); - } - } - else - { - // Set FILE_FLAG_DELETE_ON_CLOSE on the file to make sure it's deleted when the installation completes. - const wil::unique_hfile package{CreateFileW( - downloadPath.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, nullptr)}; - - THROW_LAST_ERROR_IF(!package); - - const winrt::Windows::Management::Deployment::PackageManager packageManager; - const auto result = packageManager.AddPackageAsync( - Uri{downloadPath.c_str()}, nullptr, DeploymentOptions::ForceApplicationShutdown | DeploymentOptions::ForceTargetApplicationShutdown); - - THROW_IF_FAILED(result.get().ExtendedErrorCode()); - - // Note: If the installation is successful, this process is expected to receive and Ctrl-C and exit - } - - return 0; -} - -void WaitForMsiInstall() -{ - wil::com_ptr_t installer; - - auto retry_pred = []() { - const auto errorCode = wil::ResultFromCaughtException(); - return errorCode == REGDB_E_CLASSNOTREG; - }; - - wsl::shared::retry::RetryWithTimeout( - [&installer]() { installer = wil::CoCreateInstance(__uuidof(WslInstaller), CLSCTX_LOCAL_SERVER); }, - std::chrono::seconds(1), - std::chrono::minutes(1), - retry_pred); - - fputws(wsl::shared::Localization::MessageFinishMsiInstallation().c_str(), stderr); - - auto finishLine = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { fputws(L"\n", stderr); }); - - UINT exitCode = -1; - wil::unique_cotaskmem_string message = nullptr; - THROW_IF_FAILED(installer->Install(&exitCode, &message)); - - if (*message.get() != UNICODE_NULL) - { - finishLine.release(); - wprintf(L"\n%ls\n", message.get()); - } - - if (exitCode != 0) - { - THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(exitCode), wsl::shared::Localization::MessageUpdateFailed(exitCode)); - } -} - -wil::unique_handle CreateJob() -{ - // Create a job object that will terminate all processes in the job on - // close but will not terminate the children of the processes in the job. - // This is used to ensure that when forwarding from an inbox binary (I) - // to a lifted binary (L), if I is terminated L is terminated as well but - // any children of L (e.g. wslhost.exe) continue to run. - wil::unique_handle job{CreateJobObject(nullptr, nullptr)}; - THROW_LAST_ERROR_IF_NULL(job.get()); - - JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; - info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; - THROW_IF_WIN32_BOOL_FALSE(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &info, sizeof(info))); - - return job; -} - constexpr uint16_t EndianSwap(uint16_t value) { return (value & 0xFF00) >> 8 | (value & 0x00FF) << 8; @@ -376,93 +241,6 @@ constexpr GUID EndianSwap(GUID value) } // namespace -int wsl::windows::common::wslutil::CallMsiPackage() -{ - wsl::windows::common::ExecutionContext context(wsl::windows::common::CallMsi); - - auto msiPath = GetMsiPackagePath(); - if (!msiPath.has_value()) - { - wsl::windows::common::ExecutionContext context(wsl::windows::common::Install); - - try - { - WaitForMsiInstall(); - msiPath = GetMsiPackagePath(); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - - // GetMsiPackagePath() will generate a user error if the registry access fails. - // Save the error from GetMsiPackagePath() to return a proper 'install failed' message. - auto savedError = context.ReportedError(); - - // There is a race where the service might stop before returning the install result. - // if this happens, only fail if the MSI still isn't installed. - msiPath = GetMsiPackagePath(); - if (!msiPath.has_value()) - { - // Offer to directly install the MSI package if the MsixInstaller logic fails - // This can trigger a UAC so only do it - if (IsInteractiveConsole()) - { - auto errorCode = savedError.has_value() ? ErrorToString(savedError.value()).Code - : ErrorCodeToString(wil::ResultFromCaughtException()); - - EMIT_USER_WARNING(wsl::shared::Localization::MessageInstallationCorrupted(errorCode)); - - if (PromptForKeyPressWithTimeout()) - { - return UpdatePackage(false, true); - } - } - - if (savedError.has_value()) - { - THROW_HR_WITH_USER_ERROR(savedError->Code, savedError->Message.value_or(L"")); - } - - throw; - } - } - - THROW_HR_IF(E_UNEXPECTED, !msiPath.has_value()); - } - - auto target = msiPath.value() + L"\\" WSL_BINARY_NAME; - - SubProcess process(target.c_str(), GetCommandLine()); - process.SetDesktopAppPolicy(PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE); - auto runningProcess = process.Start(); - - // N.B. The job cannot be assigned at process creation time as the packaged process - // creation path will assign the new process to a per package job object. - // In the case of multiple processes running in a single package, assigning - // the new process to the per package job object will fail for the second request - // since both jobs already have processes which prevents a job hierarchy from - // being established. - auto job = CreateJob(); - - // Assign the process to the job, ignoring failures when the process has - // terminated. - // - // N.B. Assigning the job after process creation without CREATE_SUSPENDED is - // safe to do here since only the new child process will be in the job - // object. None of the grandchildren processes are included since the - // job is created with JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK. - if (!AssignProcessToJobObject(job.get(), runningProcess.get())) - { - auto lastError = GetLastError(); - if (lastError != ERROR_ACCESS_DENIED) - { - THROW_WIN32(lastError); - } - } - - return static_cast(SubProcess::GetExitCode(runningProcess.get())); -} - template wil::com_ptr wsl::windows::common::wslutil::CoGetCallContext() { @@ -1208,21 +986,6 @@ std::vector wsl::windows::common::wslutil::ListRunningProcesses() return pids; } -void wsl::windows::common::wslutil::MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message) -{ - switch (type) - { - case INSTALLMESSAGE_ERROR: - case INSTALLMESSAGE_FATALEXIT: - case INSTALLMESSAGE_WARNING: - wprintf(L"%ls\n", message); - break; - - default: - break; - } -} - std::pair wsl::windows::common::wslutil::OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped) { // Default to 4096 byte buffer, just like CreatePipe(). @@ -1377,171 +1140,6 @@ wil::unique_hlocal_string wsl::windows::common::wslutil::SidToString(_In_ PSID U return sid; } -int WINAPI InstallRecordHandler(void* context, UINT messageType, LPCWSTR message) -{ - try - { - WSL_LOG("MSIMessage", TraceLoggingValue(messageType, "type"), TraceLoggingValue(message, "message")); - auto type = (INSTALLMESSAGE)(0xFF000000 & (UINT)messageType); - - if (type == INSTALLMESSAGE_ERROR || type == INSTALLMESSAGE_FATALEXIT || type == INSTALLMESSAGE_WARNING) - { - WriteInstallLog(std::format("MSI message: {}", message)); - } - - auto* callback = reinterpret_cast*>(context); - if (callback != nullptr) - { - (*callback)(type, message); - } - } - CATCH_LOG(); - - return IDOK; -} - -void ConfigureMsiLogging(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) -{ - if (LogFile != nullptr) - { - LOG_IF_WIN32_ERROR(MsiEnableLog(INSTALLLOGMODE_VERBOSE | INSTALLLOGMODE_EXTRADEBUG | INSTALLLOGMODE_PROGRESS, LogFile, 0)); - } - - MsiSetExternalUI( - &InstallRecordHandler, - INSTALLLOGMODE_FATALEXIT | INSTALLLOGMODE_ERROR | INSTALLLOGMODE_WARNING | INSTALLLOGMODE_USER | INSTALLLOGMODE_INFO | - INSTALLLOGMODE_RESOLVESOURCE | INSTALLLOGMODE_OUTOFDISKSPACE | INSTALLLOGMODE_ACTIONSTART | INSTALLLOGMODE_ACTIONDATA | - INSTALLLOGMODE_COMMONDATA | INSTALLLOGMODE_INITIALIZE | INSTALLLOGMODE_TERMINATE | INSTALLLOGMODE_SHOWDIALOG, - (void*)&Callback); - - MsiSetInternalUI(INSTALLUILEVEL(INSTALLUILEVEL_NONE | INSTALLUILEVEL_UACONLY | INSTALLUILEVEL_SOURCERESONLY), nullptr); -} - -int wsl::windows::common::wslutil::UpdatePackage(bool PreRelease, bool Repair) -{ - // Register a console control handler so "^C" is not printed when the app platform terminates the process. - THROW_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler( - [](DWORD ctrlType) { - if (ctrlType == CTRL_C_EVENT) - { - ExitProcess(0); - } - return FALSE; - }, - TRUE)); - - auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { SetConsoleCtrlHandler(nullptr, FALSE); }); - - try - { - return UpdatePackageImpl(PreRelease, Repair); - } - catch (...) - { - // Rethrowing via WIL is required for the error context to be properly set in case a winrt exception was thrown. - THROW_HR(wil::ResultFromCaughtException()); - } -} - -UINT wsl::windows::common::wslutil::UpgradeViaMsi( - _In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) -{ - WriteInstallLog(std::format("Upgrading via MSI package: {}. Args: {}", PackageLocation, ExtraArgs != nullptr ? ExtraArgs : L"")); - - ConfigureMsiLogging(LogFile, Callback); - - auto result = MsiInstallProduct(PackageLocation, ExtraArgs); - WSL_LOG( - "MsiInstallResult", - TraceLoggingValue(result, "result"), - TraceLoggingValue(ExtraArgs != nullptr ? ExtraArgs : L"", "ExtraArgs")); - - WriteInstallLog(std::format("MSI upgrade result: {}", result)); - - return result; -} - -UINT wsl::windows::common::wslutil::UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& Callback) -{ - const auto key = OpenLxssMachineKey(KEY_READ); - const auto productCode = ReadString(key.get(), L"Msi", L"ProductCode", nullptr); - - WriteInstallLog(std::format("Uninstalling MSI package: {}", productCode)); - - ConfigureMsiLogging(LogFile, Callback); - - auto result = MsiConfigureProduct(productCode.c_str(), 0, INSTALLSTATE_ABSENT); - WSL_LOG("MsiUninstallResult", TraceLoggingValue(result, "result")); - - WriteInstallLog(std::format("MSI package uninstall result: {}", result)); - - return result; -} - -wil::unique_hfile wsl::windows::common::wslutil::ValidateFileSignature(LPCWSTR Path) -{ - wil::unique_hfile fileHandle{CreateFileW(Path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)}; - THROW_LAST_ERROR_IF(!fileHandle); - - GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2; - WINTRUST_DATA trust{}; - trust.cbStruct = sizeof(trust); - trust.dwUIChoice = WTD_UI_NONE; - trust.dwUnionChoice = WTD_CHOICE_FILE; - trust.dwStateAction = WTD_STATEACTION_VERIFY; - - WINTRUST_FILE_INFO file = {0}; - file.cbStruct = sizeof(file); - file.hFile = fileHandle.get(); - trust.pFile = &file; - - auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - trust.dwStateAction = WTD_STATEACTION_CLOSE; - WinVerifyTrust(nullptr, &action, &trust); - }); - - THROW_IF_WIN32_ERROR(WinVerifyTrust(nullptr, &action, &trust)); - - return fileHandle; -} - -void wsl::windows::common::wslutil::WriteInstallLog(const std::string& Content) -try -{ - static std::wstring path = wil::GetWindowsDirectoryW() + L"\\temp\\wsl-install-log.txt"; - - // Wait up to 10 seconds for the log file mutex - wil::unique_handle mutex{CreateMutex(nullptr, true, L"Global\\WslInstallLog")}; - THROW_LAST_ERROR_IF(!mutex); - - THROW_LAST_ERROR_IF(WaitForSingleObject(mutex.get(), 10 * 1000) != WAIT_OBJECT_0); - - wil::unique_handle file{CreateFile( - path.c_str(), GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, 0, nullptr)}; - - THROW_LAST_ERROR_IF(!file); - - LARGE_INTEGER size{}; - THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(file.get(), &size)); - - // Append to the file if its size is below 10MB, otherwise truncate. - if (size.QuadPart < 10 * _1MB) - { - THROW_LAST_ERROR_IF(SetFilePointer(file.get(), 0, nullptr, FILE_END) == INVALID_SET_FILE_POINTER); - } - else - { - THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get())); - } - - static auto processName = wil::GetModuleFileNameW(); - auto logLine = std::format("{:%FT%TZ} {}[{}]: {}\n", std::chrono::system_clock::now(), processName, WSL_PACKAGE_VERSION, Content); - - DWORD bytesWritten{}; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), logLine.c_str(), static_cast(logLine.size()), &bytesWritten, nullptr)); -} -CATCH_LOG(); - winrt::Windows::Management::Deployment::PackageVolume wsl::windows::common::wslutil::GetSystemVolume() try { diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index a382be882..69e0e1c2a 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -70,8 +70,6 @@ void AssertValidPrintfArg() static_assert(std::is_fundamental_v || std::is_same_v || std::is_same_v || std::is_same_v); } -int CallMsiPackage(); - template wil::com_ptr CoGetCallContext(); @@ -153,8 +151,6 @@ bool IsVirtualMachinePlatformInstalled(); std::vector ListRunningProcesses(); -void MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message); - std::pair OpenAnonymousPipe(DWORD Size, bool ReadPipeOverlapped, bool WritePipeOverlapped); wil::unique_handle OpenCallingProcess(_In_ DWORD access); @@ -198,18 +194,8 @@ void SetCrtEncoding(int Mode); void SetThreadDescription(LPCWSTR Name); -wil::unique_hfile ValidateFileSignature(LPCWSTR Path); - wil::unique_hlocal_string SidToString(_In_ PSID Sid); -int UpdatePackage(bool PreRelease, bool Repair); - -UINT UpgradeViaMsi(_In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); - -UINT UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function& callback); - -void WriteInstallLog(const std::string& Content); - winrt::Windows::Management::Deployment::PackageVolume GetSystemVolume(); } // namespace wsl::windows::common::wslutil diff --git a/src/windows/service/exe/CMakeLists.txt b/src/windows/service/exe/CMakeLists.txt index 620e1276f..cfe5b8649 100644 --- a/src/windows/service/exe/CMakeLists.txt +++ b/src/windows/service/exe/CMakeLists.txt @@ -57,7 +57,9 @@ add_compile_definitions(USE_COM_CONTEXT_DEF=1) set_target_properties(wslservice PROPERTIES LINK_FLAGS "/merge:minATL=.rdata /include:__minATLObjMap_LxssUserSession_COM") target_link_libraries(wslservice ${COMMON_LINK_LIBRARIES} - computecore + ${MSI_LINK_LIBRARIES} + ${HCS_LINK_LIBRARIES} + ${SERVICE_LINK_LIBRARIES} common configfile legacy_stdio_definitions diff --git a/src/windows/service/exe/PluginManager.cpp b/src/windows/service/exe/PluginManager.cpp index b6a661e69..e4d23f226 100644 --- a/src/windows/service/exe/PluginManager.cpp +++ b/src/windows/service/exe/PluginManager.cpp @@ -13,6 +13,7 @@ Module Name: --*/ #include "precomp.h" +#include "install.h" #include "PluginManager.h" #include "WslPluginApi.h" #include "LxssUserSessionFactory.h" @@ -150,7 +151,7 @@ void PluginManager::LoadPlugin(LPCWSTR Name, LPCWSTR ModulePath) wil::unique_hfile pluginHandle; if constexpr (wsl::shared::OfficialBuild) { - pluginHandle = wsl::windows::common::wslutil::ValidateFileSignature(ModulePath); + pluginHandle = wsl::windows::common::install::ValidateFileSignature(ModulePath); WI_ASSERT(pluginHandle.is_valid()); } diff --git a/src/windows/wsl/CMakeLists.txt b/src/windows/wsl/CMakeLists.txt index ae52c0b88..13fbb1425 100644 --- a/src/windows/wsl/CMakeLists.txt +++ b/src/windows/wsl/CMakeLists.txt @@ -9,8 +9,11 @@ add_executable(wsl ${SOURCES} ${HEADERS}) target_link_libraries(wsl ${COMMON_LINK_LIBRARIES} + ${MSI_LINK_LIBRARIES} common - runtimeobject.lib) + runtimeobject.lib + delayimp.lib) +set_target_properties(wsl PROPERTIES LINK_FLAGS "/DELAYLOAD:msi.dll /DELAYLOAD:WINTRUST.dll") target_precompile_headers(wsl REUSE_FROM common) set_target_properties(wsl PROPERTIES FOLDER windows) \ No newline at end of file diff --git a/src/windows/wslg/CMakeLists.txt b/src/windows/wslg/CMakeLists.txt index 0a28c89d3..a30d4d1f8 100644 --- a/src/windows/wslg/CMakeLists.txt +++ b/src/windows/wslg/CMakeLists.txt @@ -12,7 +12,10 @@ add_dependencies(wslg target_link_libraries(wslg ${COMMON_LINK_LIBRARIES} - common) + ${MSI_LINK_LIBRARIES} + common + delayimp.lib) +set_target_properties(wslg PROPERTIES LINK_FLAGS "/DELAYLOAD:msi.dll /DELAYLOAD:WINTRUST.dll") target_precompile_headers(wslg REUSE_FROM common) set_target_properties(wslg PROPERTIES FOLDER windows) \ No newline at end of file diff --git a/src/windows/wslinstall/CMakeLists.txt b/src/windows/wslinstall/CMakeLists.txt index 921c22e3d..4f9fcf439 100644 --- a/src/windows/wslinstall/CMakeLists.txt +++ b/src/windows/wslinstall/CMakeLists.txt @@ -10,8 +10,8 @@ target_precompile_headers(wslinstall REUSE_FROM common) target_link_libraries(wslinstall ${COMMON_LINK_LIBRARIES} + ${MSI_LINK_LIBRARIES} common legacy_stdio_definitions - Msi.lib Crypt32.lib sfc.lib) \ No newline at end of file diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp index 37cc1539e..c9ab7d800 100644 --- a/src/windows/wslinstall/DllMain.cpp +++ b/src/windows/wslinstall/DllMain.cpp @@ -13,6 +13,7 @@ Module Name: --*/ #include "precomp.h" +#include "install.h" #include #include #include @@ -24,6 +25,7 @@ using unique_msi_handle = wil::unique_any InstallMsipackageImpl() } }; - auto result = wsl::windows::common::wslutil::UpgradeViaMsi( + auto result = wsl::windows::common::install::UpgradeViaMsi( GetMsiPackagePath().c_str(), L"SKIPMSIX=1", logFile.has_value() ? logFile->c_str() : nullptr, messageCallback); WSL_LOG("MSIUpgradeResult", TraceLoggingValue(result, "result"), TraceLoggingValue(errors.c_str(), "errorMessage")); @@ -140,7 +141,7 @@ std::shared_ptr LaunchInstall() return {}; } - wsl::windows::common::wslutil::WriteInstallLog(std::format("Starting upgrade via WslInstaller. Previous version: {}", existingVersion)); + wsl::windows::common::install::WriteInstallLog(std::format("Starting upgrade via WslInstaller. Previous version: {}", existingVersion)); // Return an existing install if any if (auto ptr = weak_context.lock(); ptr != nullptr) diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index 0759bb980..39bb83b72 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -24,6 +24,9 @@ target_link_libraries(wsltests common ${TAEF_LINK_LIBRARIES} ${COMMON_LINK_LIBRARIES} + ${MSI_LINK_LIBRARIES} + ${HCS_LINK_LIBRARIES} + ${SERVICE_LINK_LIBRARIES} VirtDisk.lib Wer.lib Dbghelp.lib diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 8d5e6b39f..68e760813 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -15,6 +15,7 @@ Module Name: #include "precomp.h" #include "Common.h" +#include "install.h" #include #include #include @@ -2350,7 +2351,7 @@ Error code: Wsl/InstallDistro/WSL_E_DISTRO_NOT_FOUND { LogInfo("Validating signature for: %ls", e.path().c_str()); - wsl::windows::common::wslutil::ValidateFileSignature(e.path().c_str()); + wsl::windows::common::install::ValidateFileSignature(e.path().c_str()); signedFiles++; } } From 61e6b9aa8686b6a855070668455a163e83ec925c Mon Sep 17 00:00:00 2001 From: Feng Wang Date: Sat, 21 Mar 2026 03:45:46 +0800 Subject: [PATCH 15/23] Fix wsl stuck when misconfigured cifs mount presents (#14466) * detach terminal before running mount -a * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * use _exit on error before execv in child process to avoid unintentional resource release * Add regression test * Fix clang format issue * fix all clang format issue * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * resolve ai comments * move test to unit test * Fix string literal * Overwrite fstab to resolve pipeline missing file issue --------- Co-authored-by: Feng Wang Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/linux/init/config.cpp | 2 +- src/linux/init/util.cpp | 22 +++++++++++++++++++--- src/linux/init/util.h | 3 ++- test/windows/UnitTests.cpp | 23 +++++++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index 5f6d29db2..b4a554b1c 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -2137,7 +2137,7 @@ Return Value: // const char* const Argv[] = {MOUNT_COMMAND, MOUNT_FSTAB_ARG, nullptr}; - if (UtilCreateProcessAndWait(Argv[0], Argv, nullptr, {{WSL_DRVFS_ELEVATED_ENV, Elevated ? "1" : "0"}}) < 0) + if (UtilCreateProcessAndWait(Argv[0], Argv, nullptr, {{WSL_DRVFS_ELEVATED_ENV, Elevated ? "1" : "0"}}, true) < 0) { auto message = wsl::shared::Localization::MessageFstabMountFailed(); LOG_ERROR("{}", message.c_str()); diff --git a/src/linux/init/util.cpp b/src/linux/init/util.cpp index 3cc0ae4c2..c67d69462 100644 --- a/src/linux/init/util.cpp +++ b/src/linux/init/util.cpp @@ -613,7 +613,7 @@ Return Value: return SocketFd; } -int UtilCreateProcessAndWait(const char* const File, const char* const Argv[], int* Status, const std::map& Env) +int UtilCreateProcessAndWait(const char* const File, const char* const Argv[], int* Status, const std::map& Env, bool DetachTerminal) /*++ @@ -630,6 +630,9 @@ Routine Description: Status - Supplies an optional pointer that receives the exit status of the process. + DetachTerminal - Supplies a boolean that, when true, calls setsid() in the + child process to detach it from the controlling terminal. + Return Value: 0 on success, -1 on failure. @@ -664,7 +667,7 @@ Return Value: if (UtilSetSignalHandlers(g_SavedSignalActions, false) < 0 || UtilRestoreBlockedSignals() < 0) { - exit(-1); + _exit(-1); } // @@ -676,6 +679,19 @@ Return Value: setenv(e.first.c_str(), e.second.c_str(), 1); } + // + // Detach from the controlling terminal if requested. + // + + if (DetachTerminal) + { + if (setsid() == -1) + { + LOG_ERROR("setsid failed {}", errno); + _exit(-1); + } + } + // // Invoke the executable. // @@ -686,7 +702,7 @@ Return Value: // with std::string anyway. execv(File, const_cast(Argv)); LOG_ERROR("execv({}) failed with {}", File, errno); - exit(-1); + _exit(-1); } if (Status == nullptr) diff --git a/src/linux/init/util.h b/src/linux/init/util.h index 892500b12..1fa5414d5 100644 --- a/src/linux/init/util.h +++ b/src/linux/init/util.h @@ -191,7 +191,8 @@ Return Value: _exit(1); } -int UtilCreateProcessAndWait(const char* File, const char* const Argv[], int* Status = nullptr, const std::map& Env = {}); +int UtilCreateProcessAndWait( + const char* File, const char* const Argv[], int* Status = nullptr, const std::map& Env = {}, bool DetachTerminal = false); template void UtilCreateWorkerThread(const char* Name, TMethod&& ThreadFunction) diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 68e760813..f97e96455 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -6539,5 +6539,28 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", VERIFY_ARE_EQUAL(err, L""); } + TEST_METHOD(InteractiveMount) + { + WSL2_TEST_ONLY(); + + // Add a fake interactive mount helper. + DistroFileChange mountHelper(L"/sbin/mount.hang", false); + mountHelper.SetContent( + L"#!/bin/sh\n" + L"read pass < /dev/tty\n"); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"chmod +x /sbin/mount.hang"), (DWORD)0); + + // Don't keep the original fstab as it can be missing on the pipeline. + DistroFileChange fstab(L"/etc/fstab", false); + fstab.SetContent(L"none /mnt/ttytest hang 0 0\n"); + + // Restart the distro with this mount. + WslShutdown(); + wsl::windows::common::SubProcess process(nullptr, LxssGenerateWslCommandLine(L"echo booted").c_str()); + auto result = process.RunAndCaptureOutput(60 * 1000); + VERIFY_ARE_EQUAL(result.Stdout, L"booted\n"); + VERIFY_ARE_EQUAL(result.ExitCode, 0); + } + }; // namespace UnitTests } // namespace UnitTests From 763fcbec3c0fc8e16ae9460237f2c9ebf671c636 Mon Sep 17 00:00:00 2001 From: Blue Date: Fri, 20 Mar 2026 20:46:49 +0000 Subject: [PATCH 16/23] Update localization and notice scripts to target the branch that the pipeline is running on (#14492) --- .pipelines/wsl-build-nightly-localization.yml | 8 ++++++-- .pipelines/wsl-build-notice.yml | 8 ++++++-- tools/devops/create-change.py | 5 +++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.pipelines/wsl-build-nightly-localization.yml b/.pipelines/wsl-build-nightly-localization.yml index b79bd2288..7f07be592 100644 --- a/.pipelines/wsl-build-nightly-localization.yml +++ b/.pipelines/wsl-build-nightly-localization.yml @@ -17,6 +17,9 @@ schedules: - "master" always: true +variables: + targetBranch: ${{ variables['Build.SourceBranchName'] }} + pool: vmImage: 'windows-latest' @@ -45,9 +48,10 @@ steps: - powershell: | pip install --user -r tools/devops/requirements.txt - python tools/devops/create-change.py . "$env:token" "WSL localization" "Localization change from build: $env:buildId" "user/localization/$env:buildId" + python tools/devops/create-change.py . "$env:token" "WSL localization" "Localization change from build: $env:buildId" "user/localization/$env:buildId" "$env:targetBranch" displayName: Create pull request env: token: $(GithubPRToken) - buildId: $(Build.BuildId) \ No newline at end of file + buildId: $(Build.BuildId) + targetBranch: $(targetBranch) \ No newline at end of file diff --git a/.pipelines/wsl-build-notice.yml b/.pipelines/wsl-build-notice.yml index f626c16a2..01ec662a2 100644 --- a/.pipelines/wsl-build-notice.yml +++ b/.pipelines/wsl-build-notice.yml @@ -3,6 +3,9 @@ trigger: include: - master +variables: + targetBranch: ${{ variables['Build.SourceBranchName'] }} + pool: vmImage: 'windows-latest' @@ -25,9 +28,10 @@ steps: - powershell: | pip install --user -r tools/devops/requirements.txt - python tools/devops/create-change.py . "$env:token" "WSL notice" "Notice change from build: $env:buildId" "user/notice/$env:buildId" + python tools/devops/create-change.py . "$env:token" "WSL notice" "Notice change from build: $env:buildId" "user/notice/$env:buildId" "$env:targetBranch" displayName: Create pull request env: token: $(GithubPRToken) - buildId: $(Build.BuildId) \ No newline at end of file + buildId: $(Build.BuildId) + targetBranch: $(targetBranch) \ No newline at end of file diff --git a/tools/devops/create-change.py b/tools/devops/create-change.py index a791206e1..0fd2d32f4 100644 --- a/tools/devops/create-change.py +++ b/tools/devops/create-change.py @@ -12,8 +12,9 @@ @click.argument('committer', required=True) @click.argument('message', required=True) @click.argument('branch', required=True) +@click.argument('target_branch', required=True) @click.option('--debug', default=False, is_flag=True) -def main(repo_path: str, token: str, committer: str, message: str, branch: str, debug: bool): +def main(repo_path: str, token: str, committer: str, message: str, branch: str, target_branch: str, debug: bool): try: repo = Repo(repo_path) @@ -41,7 +42,7 @@ def main(repo_path: str, token: str, committer: str, message: str, branch: str, 'title': message, 'description': 'Automated change', 'head': branch, - 'base': 'master' + 'base': target_branch } response = requests.post(f'https://api.github.com/repos/{REPO}/pulls', headers=headers, data=json.dumps(body), timeout=30) From dd88ff737c1f05cbfda5ff4286e000d53d6aafd2 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Mon, 23 Mar 2026 11:20:03 -0700 Subject: [PATCH 17/23] test: Add arm64 test distro support (#14500) * test: Add arm64 test distro support * update unit test baseline * more test baseline updates --------- Co-authored-by: Ben Hillis --- .pipelines/build-stage.yml | 10 +-- CMakeLists.txt | 2 - packages.config | 2 +- test/windows/PluginTests.cpp | 72 +++++++++++----------- test/windows/UnitTests.cpp | 13 ++-- test/windows/testplugin/Plugin.cpp | 2 +- tools/test/Microsoft.WSL.TestDistro.nuspec | 5 +- tools/test/build-test-distro.ps1 | 18 ++++-- tools/test/setup-vm-for-tests.ps1 | 2 +- tools/test/test.bat.in | 2 +- 10 files changed, 68 insertions(+), 60 deletions(-) diff --git a/.pipelines/build-stage.yml b/.pipelines/build-stage.yml index 09bc7620f..8aa4e92ab 100644 --- a/.pipelines/build-stage.yml +++ b/.pipelines/build-stage.yml @@ -382,24 +382,26 @@ stages: - powershell: | $taefVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.Taef'']/@version').Node.Value New-Item -ItemType Directory -Path "$(ob_outputDirectory)\bundle" -Force - foreach ($arch in @("x64", "ARM64")) + # TODO: Add "ARM64" to this list once arm64 testing is supported + foreach ($arch in @("x64")) { mkdir $(ob_outputDirectory)\testbin\$arch\release Move-Item -Path "bin\$arch\release\wsltests.dll" -Destination "$(ob_outputDirectory)\testbin\$arch\release\wsltests.dll" Move-Item -Path "bin\$arch\release\testplugin.dll" -Destination "$(ob_outputDirectory)\testbin\$arch\release\testplugin.dll" Move-Item -Path "packages\Microsoft.Taef.$taefVersion\build\Binaries\$arch" -Destination "$(ob_outputDirectory)\testbin\$arch\release\taef" + Move-Item -Path "bin\$arch\cloudtest" -Destination "$(ob_outputDirectory)\testbin\$arch\cloudtest" + + $TestDistroVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.WSL.TestDistro'']/@version').Node.Value + Copy-Item "packages\Microsoft.WSL.TestDistro.$TestDistroVersion\$arch\test_distro.tar.xz" "$(ob_outputDirectory)\testbin\$arch" } - Move-Item -Path "bin\x64\cloudtest" -Destination "$(ob_outputDirectory)\testbin\x64\cloudtest" Move-Item -Path "tools\test\test-setup.ps1" -Destination "$(ob_outputDirectory)\testbin\test-setup.ps1" Move-Item -Path "tools\test\CloudTest-Setup.bat" -Destination "$(ob_outputDirectory)\testbin\CloudTest-Setup.bat" Move-Item -Path "diagnostics\wsl.wprp" -Destination "$(ob_outputDirectory)\testbin\wsl.wprp" Move-Item -Path "test\linux\unit_tests" -Destination "$(ob_outputDirectory)\testbin\unit_tests" Move-Item -Path bundle\release\* -Destination $(ob_outputDirectory)\bundle - $TestDistroVersion = (Select-Xml -Path packages.config -XPath '/packages/package[@id=''Microsoft.WSL.TestDistro'']/@version').Node.Value - Copy-Item "packages\Microsoft.WSL.TestDistro.$TestDistroVersion\test_distro.tar.xz" "$(ob_outputDirectory)\testbin\x64" displayName: Move artifacts to drop directory diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a1701b70..bab62f64d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,10 +16,8 @@ endif() if("${CMAKE_GENERATOR_PLATFORM}" STREQUAL "arm64" OR "${TARGET_PLATFORM}" STREQUAL "arm64") set(TARGET_PLATFORM "arm64") - set(TEST_DISTRO_PLATFORM "arm64") elseif("${CMAKE_GENERATOR_PLATFORM}" MATCHES "x64|amd64" OR "${TARGET_PLATFORM}" MATCHES "x64|amd64") set(TARGET_PLATFORM "x64") - set(TEST_DISTRO_PLATFORM "amd64") else() message(FATAL_ERROR "Unsupported platform: ${CMAKE_GENERATOR_PLATFORM}") endif() diff --git a/packages.config b/packages.config index 619b94c96..0bc282ab1 100644 --- a/packages.config +++ b/packages.config @@ -21,7 +21,7 @@ - + diff --git a/test/windows/PluginTests.cpp b/test/windows/PluginTests.cpp index 036efdfe8..1b5f2da75 100644 --- a/test/windows/PluginTests.cpp +++ b/test/windows/PluginTests.cpp @@ -159,8 +159,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); @@ -182,8 +182,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); @@ -200,8 +200,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::Success); @@ -259,8 +259,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=2) Folder mounted (* -> /test-plugin) Process created - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; WslConfigChange config(LxssGenerateTestConfig({.vmIdleTimeout = 1, .kernelCommandLine = L"custom"})); @@ -277,13 +277,13 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=10 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: received same GUID - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::SameDistroId); @@ -301,11 +301,11 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=14 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Distribution Stopping, name=test_distro, package=, PidNs=* - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Init's pid is different (* ! = *) - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::InitPidIsDifferent); @@ -341,8 +341,8 @@ class PluginTests LR"(Plugin loaded. TestMode=7 VM created (settings->CustomConfigurationFlags=0) API error tests passed - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; ConfigurePlugin(PluginTestType::ApiErrors); @@ -442,8 +442,8 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=5 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping OnVmStopping: E_UNEXPECTED)"; @@ -459,7 +459,7 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=4 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: E_UNEXPECTED VM Stopping)"; @@ -479,8 +479,8 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=6 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 OnDistroStopping: E_UNEXPECTED VM Stopping)"; @@ -515,7 +515,7 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=12 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 OnDistroStarted: E_FAIL VM Stopping)"; @@ -543,8 +543,8 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=12 - Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 + Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM Stopping)"; ValidateLogFile(ExpectedOutput); @@ -568,14 +568,14 @@ class PluginTests VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM Stopping - Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 VM created (settings->CustomConfigurationFlags=0) Folder mounted (* -> /test-plugin) Process created - Distribution registered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=12 - Distribution unregistered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13 + Distribution unregistered, name=plugin-test-distro-vhd, package=, Flavor=debian, Version=13 VM Stopping)"; ValidateLogFile(ExpectedOutput); @@ -593,9 +593,9 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=15 VM created (settings->CustomConfigurationFlags=0) - Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution registered, name=plugin-test-distro, package=, Flavor=debian, Version=13 OnDistributionRegistered: E_UNEXPECTED - Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=12 + Distribution unregistered, name=plugin-test-distro, package=, Flavor=debian, Version=13 OnDistributionUnregistered: E_UNEXPECTED VM Stopping)"; @@ -611,11 +611,11 @@ class PluginTests constexpr auto ExpectedOutput = LR"(Plugin loaded. TestMode=16 VM created (settings->CustomConfigurationFlags=0) - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 Process created Failed process launch returned: -2147467259 Invalid distro launch returned: -2147220717 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; StartWsl(0); @@ -632,8 +632,8 @@ class PluginTests LR"(Plugin loaded. TestMode=17 VM created (settings->CustomConfigurationFlags=0) Username: * - Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=12 - Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=12 + Distribution started, name=test_distro, package=, PidNs=*, InitPid=*, Flavor=debian, Version=13 + Distribution Stopping, name=test_distro, package=, PidNs=*, Flavor=debian, Version=13 VM Stopping)"; StartWsl(0); @@ -666,4 +666,4 @@ class PluginTests L"A fatal error was returned by plugin 'TestPlugin'\r\nError code: " L"Wsl/Service/CreateInstance/CreateVm/Plugin/TRUST_E_NOSIGNATURE\r\n"); } -}; \ No newline at end of file +}; diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index f97e96455..667ed1dc8 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -355,7 +355,7 @@ class UnitTests WSL2_TEST_ONLY(); // Override WSL's binfmt interpreter - VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"echo ':WSLInterop:M::MZ::/bin/echo:PF' > /usr/lib/binfmt.d/dummy.conf"), 0L); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"mkdir -p /usr/lib/binfmt.d && echo ':WSLInterop:M::MZ::/bin/echo:PF' > /usr/lib/binfmt.d/dummy.conf"), 0L); auto cleanupBinfmt = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { LxsstuLaunchWsl(L"rm /usr/lib/binfmt.d/dummy.conf"); @@ -3807,7 +3807,7 @@ localhostForwarding=true validateUidChange(L"root", 0, L"The operation completed successfully. \r\n", L"", 0); const std::wstring invalidUser = L"Nonexistent"; - validateUidChange(invalidUser, 0, L"", L"/usr/bin/id: \u2018" + invalidUser + L"\u2019: no such user\n", 1); + validateUidChange(invalidUser, 0, L"", L"id: \u2018" + invalidUser + L"\u2019: no such user\n", 1); auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--manage nonexistent --set-default-user root", -1); @@ -3931,7 +3931,7 @@ localhostForwarding=true VERIFY_ARE_EQUAL(ExpectedVersion, version); }; - validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"debian", L"12"); + validateFlavorVersion(LXSS_DISTRO_NAME_TEST_L, L"debian", L"13"); constexpr auto testTar = L"exported-distro.tar"; constexpr auto tmpDistroName = L"tmpdistro"; @@ -4031,10 +4031,10 @@ VERSION_ID="Invalid|Format" // Verify that importing a distribution with an os-release as then converting works as well VERIFY_ARE_EQUAL( LxsstuLaunchWsl(std::format(L"--import {} . {} --version {}", tmpDistroName, g_testDistroPath, convertVersion).c_str()), 0L); - validateFlavorVersion(tmpDistroName, L"debian", L"12"); + validateFlavorVersion(tmpDistroName, L"debian", L"13"); VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"--set-version {} {}", tmpDistroName, currentVersion).c_str()), 0L); - validateFlavorVersion(tmpDistroName, L"debian", L"12"); + validateFlavorVersion(tmpDistroName, L"debian", L"13"); } TEST_METHOD(DistributionId) @@ -6410,8 +6410,7 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", // See https://github.com/microsoft/WSL/issues/13173. TEST_METHOD(SetSidNoWarning) { - auto [out, err] = - LxsstuLaunchWslAndCaptureOutput(L"socat - 'EXEC:setsid --wait cmd.exe /c echo OK',pty,setsid,ctty,stderr"); + auto [out, err] = LxsstuLaunchWslAndCaptureOutput(L"socat - 'EXEC:setsid --wait cmd.exe /c echo OK',pty,setsid,stderr"); VERIFY_ARE_EQUAL(out, L"OK\r\r\n"); VERIFY_ARE_EQUAL(err, L""); diff --git a/test/windows/testplugin/Plugin.cpp b/test/windows/testplugin/Plugin.cpp index 50be9e741..b7cdc3815 100644 --- a/test/windows/testplugin/Plugin.cpp +++ b/test/windows/testplugin/Plugin.cpp @@ -261,7 +261,7 @@ HRESULT OnDistroStarted(const WSLSessionInformation* Session, const WSLDistribut // Validate that the process actually ran inside the distro. auto output = ReadFromSocket(socket.get()); - const auto expected = "Debian GNU/Linux 12\n"; + const auto expected = "Debian GNU/Linux 13\n"; if (std::string(output.begin(), output.end()) != expected) { g_logfile << "Got unexpected output from bash: " << std::string(output.begin(), output.end()) diff --git a/tools/test/Microsoft.WSL.TestDistro.nuspec b/tools/test/Microsoft.WSL.TestDistro.nuspec index 4393526c7..05d18dc04 100644 --- a/tools/test/Microsoft.WSL.TestDistro.nuspec +++ b/tools/test/Microsoft.WSL.TestDistro.nuspec @@ -8,6 +8,7 @@ WSL Test distribution - + + - \ No newline at end of file + diff --git a/tools/test/build-test-distro.ps1 b/tools/test/build-test-distro.ps1 index 3918adccc..8b12ba5bd 100644 --- a/tools/test/build-test-distro.ps1 +++ b/tools/test/build-test-distro.ps1 @@ -8,7 +8,7 @@ #> [CmdletBinding()] -Param ($Base) +Param ($Base, [string]$Platform) $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest @@ -32,10 +32,18 @@ function RunInDistro { } } +if ([string]::IsNullOrEmpty($Platform)) { + if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { + $Platform = "arm64" + } else { + $Platform = "x64" + } +} + $git_version = (git describe --tags).split('-') $version = "$($git_version[0])-$($git_version[1])" -echo "Building test_distro version: $version" +echo "Building test_distro version: $version for $Platform" Run { wsl.exe --install $Base --name test_distro --version 2 --no-launch } @@ -50,7 +58,7 @@ RunInDistro("rm -rf /etc/wsl-distribution.conf /etc/wsl.conf /usr/share/doc/* /v RunInDistro('rm -rf -- $(ls /usr/share/locale ^| grep -vE "en|locale.alias")') RunInDistro("rm /usr/lib/systemd/user/{systemd-tmpfiles-setup.service,systemd-tmpfiles-clean.timer,systemd-tmpfiles-clean.service}") RunInDistro("rm /usr/lib/systemd/system/{systemd-tmpfiles-setup-dev.service,systemd-tmpfiles-setup.service,systemd-tmpfiles-clean.timer,systemd-tmpfiles-clean.service} /lib/systemd/system/{kmod-static-nodes.service,kmod.service,sysinit.target.wants/kmod-static-nodes.service}") -Run { wsl.exe --export test_distro "test_distro.tar" } -Run { wsl.exe xz -e9 "test_distro.tar" } +New-Item -ItemType Directory -Path $Platform -Force +Run { wsl.exe --export --format tar.xz test_distro "$Platform\test_distro.tar.xz" } -& "$PSScriptRoot/../../_deps/nuget.exe" pack Microsoft.WSL.TestDistro.nuspec -Properties version=$version \ No newline at end of file +& "$PSScriptRoot/../../_deps/nuget.exe" pack Microsoft.WSL.TestDistro.nuspec -Properties version=$version diff --git a/tools/test/setup-vm-for-tests.ps1 b/tools/test/setup-vm-for-tests.ps1 index 335d53e75..cab784548 100644 --- a/tools/test/setup-vm-for-tests.ps1 +++ b/tools/test/setup-vm-for-tests.ps1 @@ -95,7 +95,7 @@ $Platform = switch ($Arch) { if ([string]::IsNullOrEmpty($TestDistroPath)) { $TestDistroVersion = (Select-Xml -Path "$PSScriptRoot\..\..\packages.config" -XPath '/packages/package[@id=''Microsoft.WSL.TestDistro'']/@version').Node.Value - $TestDistroPath = "$PSScriptRoot\..\..\packages\Microsoft.WSL.TestDistro.$TestDistroVersion\test_distro.tar.xz" + $TestDistroPath = "$PSScriptRoot\..\..\packages\Microsoft.WSL.TestDistro.$TestDistroVersion\$Platform\test_distro.tar.xz" } if ([string]::IsNullOrEmpty($ArtifactFolder)) { diff --git a/tools/test/test.bat.in b/tools/test/test.bat.in index 767d122e2..733c884ca 100644 --- a/tools/test/test.bat.in +++ b/tools/test/test.bat.in @@ -6,7 +6,7 @@ set "Bin=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}\${CMAKE_BUILD_TYPE}" set "Tools=${CMAKE_CURRENT_LIST_DIR}\tools" path %PATH%;${TAEF_SOURCE_DIR}\build\Binaries\${TARGET_PLATFORM} -powershell.exe -ExecutionPolicy Bypass "%tools%\test\run-tests.ps1" -SetupScript "%tools%\test\test-setup.ps1" -DistroPath "${TEST_DISTRO_SOURCE_DIR}test_distro.tar.xz" -TestDllPath "%bin%\wsltests.dll" -UnitTestsPath "${CMAKE_CURRENT_LIST_DIR}\test\linux\unit_tests" -Package "%bin%\installer.msix" %* || goto fail +powershell.exe -ExecutionPolicy Bypass "%tools%\test\run-tests.ps1" -SetupScript "%tools%\test\test-setup.ps1" -DistroPath "${TEST_DISTRO_SOURCE_DIR}${TARGET_PLATFORM}\test_distro.tar.xz" -TestDllPath "%bin%\wsltests.dll" -UnitTestsPath "${CMAKE_CURRENT_LIST_DIR}\test\linux\unit_tests" -Package "%bin%\installer.msix" %* || goto fail exit /b 0 From 2b510027056c546cac9b11e4305f1a0ed3df9d70 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 24 Mar 2026 19:22:36 -0700 Subject: [PATCH 18/23] test: remove duplicated DNS test coverage (#14522) * test: remove duplicated DNS test coverage * format source --------- Co-authored-by: Ben Hillis --- test/windows/Common.cpp | 4 ++-- test/windows/NetworkTests.cpp | 30 ------------------------------ 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index de742a08f..99cf3cf45 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -250,9 +250,9 @@ Return Value: { THROW_HR_MSG( E_UNEXPECTED, - "Command \"%ls\"" + "Command \"%ls\" " "returned unexpected exit code (%lu != %i). " - "Stdout: '%ls'" + "Stdout: '%ls' " "Stderr: '%ls'", Cmd, ExitCode, diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 31135565b..126aeec37 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -809,19 +809,6 @@ class NetworkTests VerifyDigDnsResolution(L"dig +short +time=5 SOA bing.com"); } - static void VerifyDnsResolutionDigV6() - { - if (!HostHasInternetConnectivity(AF_INET6)) - { - LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); - return; - } - - // Test AAAA record resolution (IPv6) with both UDP and TCP - VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com"); - VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com"); - } - static void VerifyDnsQueries() { // query for A/IPv4 records @@ -5058,15 +5045,6 @@ class VirtioProxyTests NetworkTests::VerifyDnsResolutionRecordTypes(); } - TEST_METHOD(DnsResolutionDigV6DnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionDigV6(); - } - TEST_METHOD(DnsResolutionBasic) { VIRTIOPROXY_TEST_ONLY(); @@ -5090,13 +5068,5 @@ class VirtioProxyTests m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); NetworkTests::VerifyDnsResolutionRecordTypes(); } - - TEST_METHOD(DnsResolutionDigV6) - { - VIRTIOPROXY_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); - NetworkTests::VerifyDnsResolutionDigV6(); - } }; } // namespace NetworkTests From 83d59d4b4a31b457183caa02d11dff5e5c328d16 Mon Sep 17 00:00:00 2001 From: Feng Wang Date: Wed, 25 Mar 2026 13:08:56 +0800 Subject: [PATCH 19/23] Fix: Fail and warn the user when --uninstall is given parameters (#14524) Fail and warn the user when --uninstall is given parameters. --- localization/strings/en-US/Resources.resw | 4 ++++ src/windows/common/WslClient.cpp | 9 +++++++++ test/windows/UnitTests.cpp | 8 ++++++++ 3 files changed, 21 insertions(+) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 55a700b83..1a9b8cbd8 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -960,6 +960,10 @@ Falling back to NAT networking. Update failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Uninstall failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/src/windows/common/WslClient.cpp b/src/windows/common/WslClient.cpp index 49cdb5ef7..e1f3a0f1f 100644 --- a/src/windows/common/WslClient.cpp +++ b/src/windows/common/WslClient.cpp @@ -1751,6 +1751,15 @@ int WslMain(_In_ std::wstring_view commandLine) } else if (argument == WSL_UNINSTALL_ARG) { + commandLine = wsl::windows::common::helpers::ConsumeArgument(commandLine, argument); + argument = wsl::windows::common::helpers::ParseArgument(commandLine); + if (!argument.empty()) + { + wsl::windows::common::wslutil::PrintMessage( + Localization::MessageUninstallNoArguments(WSL_UNINSTALL_ARG, WSL_UNREGISTER_ARG), stdout); + return exitCode; + } + return Uninstall(); } else diff --git a/test/windows/UnitTests.cpp b/test/windows/UnitTests.cpp index 667ed1dc8..9d9bf189c 100644 --- a/test/windows/UnitTests.cpp +++ b/test/windows/UnitTests.cpp @@ -6561,5 +6561,13 @@ Error code: Wsl/InstallDistro/WSL_E_INVALID_JSON\r\n", VERIFY_ARE_EQUAL(result.ExitCode, 0); } + TEST_METHOD(UninstallRejectsArguments) + { + VerifyOutput( + L"--uninstall Distro", + wsl::shared::Localization::MessageUninstallNoArguments(WSL_UNINSTALL_ARG, WSL_UNREGISTER_ARG) + L"\r\n", + -1); + } + }; // namespace UnitTests } // namespace UnitTests From 8338ecaf527c00786aec82b23bb749e3a62694bc Mon Sep 17 00:00:00 2001 From: Blue Date: Wed, 25 Mar 2026 08:23:15 -0700 Subject: [PATCH 20/23] Localization change from build: 142847827 (#14525) Co-authored-by: WSL localization --- localization/strings/cs-CZ/Resources.resw | 4 ++++ localization/strings/da-DK/Resources.resw | 4 ++++ localization/strings/de-DE/Resources.resw | 4 ++++ localization/strings/en-GB/Resources.resw | 4 ++++ localization/strings/es-ES/Resources.resw | 4 ++++ localization/strings/fi-FI/Resources.resw | 4 ++++ localization/strings/fr-FR/Resources.resw | 4 ++++ localization/strings/hu-HU/Resources.resw | 4 ++++ localization/strings/it-IT/Resources.resw | 4 ++++ localization/strings/ja-JP/Resources.resw | 4 ++++ localization/strings/ko-KR/Resources.resw | 4 ++++ localization/strings/nb-NO/Resources.resw | 4 ++++ localization/strings/nl-NL/Resources.resw | 4 ++++ localization/strings/pl-PL/Resources.resw | 4 ++++ localization/strings/pt-BR/Resources.resw | 4 ++++ localization/strings/pt-PT/Resources.resw | 4 ++++ localization/strings/ru-RU/Resources.resw | 4 ++++ localization/strings/sv-SE/Resources.resw | 4 ++++ localization/strings/tr-TR/Resources.resw | 4 ++++ localization/strings/zh-CN/Resources.resw | 4 ++++ localization/strings/zh-TW/Resources.resw | 4 ++++ 21 files changed, 84 insertions(+) diff --git a/localization/strings/cs-CZ/Resources.resw b/localization/strings/cs-CZ/Resources.resw index ce5b44f5a..9627ba637 100644 --- a/localization/strings/cs-CZ/Resources.resw +++ b/localization/strings/cs-CZ/Resources.resw @@ -960,6 +960,10 @@ Návrat k sítím NAT. Aktualizace se nezdařila (ukončovací kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Odinstalování se nezdařilo (ukončovací kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/da-DK/Resources.resw b/localization/strings/da-DK/Resources.resw index e67fea572..a9b8e0d34 100644 --- a/localization/strings/da-DK/Resources.resw +++ b/localization/strings/da-DK/Resources.resw @@ -960,6 +960,10 @@ Går tilbage til NAT-netværk. Opdateringen mislykkedes (afslutningskode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Fjernelsen mislykkedes (udgangskode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/de-DE/Resources.resw b/localization/strings/de-DE/Resources.resw index 21e3e7859..4d8b25793 100644 --- a/localization/strings/de-DE/Resources.resw +++ b/localization/strings/de-DE/Resources.resw @@ -966,6 +966,10 @@ Fallback auf NAT-Netzwerk. Fehler beim Aktualisieren (Exitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Fehler bei der Deinstallation (Exitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/en-GB/Resources.resw b/localization/strings/en-GB/Resources.resw index 75b481fac..585132540 100644 --- a/localization/strings/en-GB/Resources.resw +++ b/localization/strings/en-GB/Resources.resw @@ -960,6 +960,10 @@ Falling back to NAT networking. Update failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Uninstall failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/es-ES/Resources.resw b/localization/strings/es-ES/Resources.resw index 852222d2b..0ac7874ad 100644 --- a/localization/strings/es-ES/Resources.resw +++ b/localization/strings/es-ES/Resources.resw @@ -966,6 +966,10 @@ Revirtiendo a las redes NAT. Error de actualización (código de salida: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Error de desinstalación (código de salida: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/fi-FI/Resources.resw b/localization/strings/fi-FI/Resources.resw index c249bcf6e..88185fbe8 100644 --- a/localization/strings/fi-FI/Resources.resw +++ b/localization/strings/fi-FI/Resources.resw @@ -960,6 +960,10 @@ Palataan nat-verkkopalveluun. Päivitys epäonnistui (lopetuskoodi: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Asennuksen poistaminen epäonnistui (lopetuskoodi: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/fr-FR/Resources.resw b/localization/strings/fr-FR/Resources.resw index 05cdbaefa..43887e76c 100644 --- a/localization/strings/fr-FR/Resources.resw +++ b/localization/strings/fr-FR/Resources.resw @@ -967,6 +967,10 @@ Retour à la mise en réseau NAT. Échec de la mise à jour (code de sortie : {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Échec de la désinstallation (code de sortie : {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/hu-HU/Resources.resw b/localization/strings/hu-HU/Resources.resw index 59ab0266a..caaabb069 100644 --- a/localization/strings/hu-HU/Resources.resw +++ b/localization/strings/hu-HU/Resources.resw @@ -960,6 +960,10 @@ Visszaállás NAT-hálózatkezelésre. A frissítés nem sikerült (kilépési kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Az eltávolítás nem sikerült (kilépési kód: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/it-IT/Resources.resw b/localization/strings/it-IT/Resources.resw index a0045c19c..f1ee2e0b8 100644 --- a/localization/strings/it-IT/Resources.resw +++ b/localization/strings/it-IT/Resources.resw @@ -966,6 +966,10 @@ Fallback alla rete NAT. Aggiornamento non riuscito (codice di uscita: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Disinstallazione non riuscita (codice di uscita: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/ja-JP/Resources.resw b/localization/strings/ja-JP/Resources.resw index bc69e12ea..2765410f7 100644 --- a/localization/strings/ja-JP/Resources.resw +++ b/localization/strings/ja-JP/Resources.resw @@ -966,6 +966,10 @@ NAT ネットワークにフォールバックしています。 更新に失敗しました (終了コード: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + アンインストールに失敗しました (終了コード: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/ko-KR/Resources.resw b/localization/strings/ko-KR/Resources.resw index 0ccd6849e..6128f8f51 100644 --- a/localization/strings/ko-KR/Resources.resw +++ b/localization/strings/ko-KR/Resources.resw @@ -966,6 +966,10 @@ NAT 네트워킹으로 대체합니다. 업데이트하지 못했습니다(종료 코드: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + 제거하지 못했습니다(종료 코드: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/nb-NO/Resources.resw b/localization/strings/nb-NO/Resources.resw index ff222e53b..45d566e64 100644 --- a/localization/strings/nb-NO/Resources.resw +++ b/localization/strings/nb-NO/Resources.resw @@ -960,6 +960,10 @@ Faller tilbake til NAT-nettverk. Oppdateringen mislyktes (feilkode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Avinstalleringen mislyktes (feilkode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/nl-NL/Resources.resw b/localization/strings/nl-NL/Resources.resw index d7af70dda..0e94fcd8a 100644 --- a/localization/strings/nl-NL/Resources.resw +++ b/localization/strings/nl-NL/Resources.resw @@ -960,6 +960,10 @@ Terugvallen op NAT-netwerken. Bijwerken is mislukt (afsluitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Installatie ongedaan maken is mislukt (afsluitcode: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/pl-PL/Resources.resw b/localization/strings/pl-PL/Resources.resw index 4490ce23e..8a673c475 100644 --- a/localization/strings/pl-PL/Resources.resw +++ b/localization/strings/pl-PL/Resources.resw @@ -960,6 +960,10 @@ Powrót do sieci NAT. Aktualizacja nie powiodła się (kod zakończenia: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Odinstalowanie nie powiodło się (kod zakończenia: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/pt-BR/Resources.resw b/localization/strings/pt-BR/Resources.resw index 7081b176f..c0f64c911 100644 --- a/localization/strings/pt-BR/Resources.resw +++ b/localization/strings/pt-BR/Resources.resw @@ -967,6 +967,10 @@ Voltando à rede NAT. Falha na atualização (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Falha ao desinstalar (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/pt-PT/Resources.resw b/localization/strings/pt-PT/Resources.resw index 03eacf57a..13c77abb4 100644 --- a/localization/strings/pt-PT/Resources.resw +++ b/localization/strings/pt-PT/Resources.resw @@ -960,6 +960,10 @@ A reverter para a rede NAT. A atualização falhou (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Falha ao desinstalar (código de saída: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/ru-RU/Resources.resw b/localization/strings/ru-RU/Resources.resw index eae84ab0c..f0c9dff3e 100644 --- a/localization/strings/ru-RU/Resources.resw +++ b/localization/strings/ru-RU/Resources.resw @@ -967,6 +967,10 @@ Сбой обновления (код выхода: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Не удалось удалить (код выхода: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/sv-SE/Resources.resw b/localization/strings/sv-SE/Resources.resw index ec45bca4a..8b2608831 100644 --- a/localization/strings/sv-SE/Resources.resw +++ b/localization/strings/sv-SE/Resources.resw @@ -960,6 +960,10 @@ Mer information finns på https://aka.ms/wslinstall Uppdateringen misslyckades (slutkod: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Avinstallationen misslyckades (slutkod: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/tr-TR/Resources.resw b/localization/strings/tr-TR/Resources.resw index 87475b143..1e857cc42 100644 --- a/localization/strings/tr-TR/Resources.resw +++ b/localization/strings/tr-TR/Resources.resw @@ -960,6 +960,10 @@ NAT ağ bağlantısına geri dönülüyor. Güncelleştirme başarısız (çıkış kodu: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Kaldırma başarısız oldu (çıkış kodu: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/zh-CN/Resources.resw b/localization/strings/zh-CN/Resources.resw index 30f2e9a36..3388a0fc6 100644 --- a/localization/strings/zh-CN/Resources.resw +++ b/localization/strings/zh-CN/Resources.resw @@ -966,6 +966,10 @@ Windows: {} 更新失败(退出代码: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + 卸载失败(退出代码: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/localization/strings/zh-TW/Resources.resw b/localization/strings/zh-TW/Resources.resw index 933030fc1..c87de1b46 100644 --- a/localization/strings/zh-TW/Resources.resw +++ b/localization/strings/zh-TW/Resources.resw @@ -966,6 +966,10 @@ Windows 版本: {} 更新失敗 (結束代碼: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + {} does not take any arguments. To unregister a distribution, use {} instead. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + 解除安裝失敗 (結束代碼: {})。 {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated From 7370bf2bb53ded977bfbc1302bae5f8d7f5d55f1 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 25 Mar 2026 13:30:35 -0700 Subject: [PATCH 21/23] virito net: revert to previous DNS behavior while we debug an issue with DNS over TCP (#14532) Co-authored-by: Ben Hillis --- src/windows/common/WslCoreConfig.cpp | 14 +++++--------- src/windows/service/exe/WslCoreVm.cpp | 7 ++----- test/windows/NetworkTests.cpp | 27 --------------------------- 3 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/windows/common/WslCoreConfig.cpp b/src/windows/common/WslCoreConfig.cpp index 26c1a4b40..ee8b780aa 100644 --- a/src/windows/common/WslCoreConfig.cpp +++ b/src/windows/common/WslCoreConfig.cpp @@ -474,19 +474,15 @@ void wsl::core::Config::Initialize(_In_opt_ HANDLE UserToken) EnableVirtio9p = false; } - if (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored && NetworkingMode != NetworkingMode::VirtioProxy) + if (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored) { - VALIDATE_CONFIG_OPTION( - (NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored && NetworkingMode != NetworkingMode::VirtioProxy), - EnableDnsTunneling, - false); + VALIDATE_CONFIG_OPTION((NetworkingMode != NetworkingMode::Nat && NetworkingMode != NetworkingMode::Mirrored), EnableDnsTunneling, false); } - if (!EnableDnsTunneling || NetworkingMode == NetworkingMode::VirtioProxy) + if (!EnableDnsTunneling) { - VALIDATE_CONFIG_OPTION(!EnableDnsTunneling || NetworkingMode == NetworkingMode::VirtioProxy, BestEffortDnsParsing, false); - VALIDATE_CONFIG_OPTION( - !EnableDnsTunneling || NetworkingMode == NetworkingMode::VirtioProxy, DnsTunnelingIpAddress, std::optional{}); + VALIDATE_CONFIG_OPTION(!EnableDnsTunneling, BestEffortDnsParsing, false); + VALIDATE_CONFIG_OPTION(!EnableDnsTunneling, DnsTunnelingIpAddress, std::optional{}); } if (NetworkingMode != NetworkingMode::Mirrored) diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index adfc44a1f..2d26d2bee 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -520,7 +520,7 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken message->MemoryReclaimMode = static_cast(m_vmConfig.MemoryReclaim); message->EnableDebugShell = m_vmConfig.EnableDebugShell; message->EnableSafeMode = m_vmConfig.EnableSafeMode; - message->EnableDnsTunneling = m_vmConfig.EnableDnsTunneling && m_vmConfig.NetworkingMode != NetworkingMode::VirtioProxy; + message->EnableDnsTunneling = m_vmConfig.EnableDnsTunneling; message->DefaultKernel = m_defaultKernel; message->KernelModulesDeviceId = m_kernelModulesDeviceId; message.WriteString(message->HostnameOffset, wsl::windows::common::filesystem::GetLinuxHostName()); @@ -578,7 +578,6 @@ void WslCoreVm::Initialize(const GUID& VmId, const wil::shared_handle& UserToken { wsl::core::VirtioNetworkingFlags flags = wsl::core::VirtioNetworkingFlags::Ipv6; WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::LocalhostRelay, m_vmConfig.EnableLocalhostRelay); - WI_SetFlagIf(flags, wsl::core::VirtioNetworkingFlags::DnsTunneling, m_vmConfig.EnableDnsTunneling); m_networkingEngine = std::make_unique( std::move(gnsChannel), flags, LX_INIT_RESOLVCONF_FULL_HEADER, m_guestDeviceManager, m_userToken); } @@ -1932,9 +1931,7 @@ bool WslCoreVm::InitializeDrvFsLockHeld(_In_ HANDLE UserToken) bool WslCoreVm::IsDnsTunnelingSupported() const { - WI_ASSERT( - m_vmConfig.NetworkingMode == NetworkingMode::Nat || m_vmConfig.NetworkingMode == NetworkingMode::Mirrored || - m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy); + WI_ASSERT(m_vmConfig.NetworkingMode == NetworkingMode::Nat || m_vmConfig.NetworkingMode == NetworkingMode::Mirrored); return SUCCEEDED_LOG(wsl::core::networking::DnsResolver::LoadDnsResolverMethods()); } diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 126aeec37..b8a85ab5f 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -5018,33 +5018,6 @@ class VirtioProxyTests VERIFY_IS_TRUE(std::regex_match(out, v6Pattern)); } - TEST_METHOD(DnsResolutionBasicDnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionBasic(); - } - - TEST_METHOD(DnsResolutionDigDnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionDig(); - } - - TEST_METHOD(DnsResolutionRecordTypesDnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionRecordTypes(); - } - TEST_METHOD(DnsResolutionBasic) { VIRTIOPROXY_TEST_ONLY(); From 9564cc4a33edd6d726d604a09e04386f4df8b784 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 25 Mar 2026 15:10:25 -0700 Subject: [PATCH 22/23] devicehost: update to latest devicehost nuget with tracing improvements (#14531) Co-authored-by: Ben Hillis --- packages.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages.config b/packages.config index 0bc282ab1..2f1df48f7 100644 --- a/packages.config +++ b/packages.config @@ -18,7 +18,7 @@ - + From 5442544989f9d7abd273776ffdb778e62b66718b Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Wed, 25 Mar 2026 17:23:16 -0700 Subject: [PATCH 23/23] fix merge issues --- src/windows/common/CMakeLists.txt | 2 +- test/windows/NetworkTests.cpp | 57 ------------------------------- 2 files changed, 1 insertion(+), 58 deletions(-) diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index f3fa589c4..79ac5a036 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -49,7 +49,7 @@ set(SOURCES WslTelemetry.cpp wslutil.cpp install.cpp - notifications.cpp) + ) set(HEADERS ../../../generated/Localization.h diff --git a/test/windows/NetworkTests.cpp b/test/windows/NetworkTests.cpp index 31135565b..b8a85ab5f 100644 --- a/test/windows/NetworkTests.cpp +++ b/test/windows/NetworkTests.cpp @@ -809,19 +809,6 @@ class NetworkTests VerifyDigDnsResolution(L"dig +short +time=5 SOA bing.com"); } - static void VerifyDnsResolutionDigV6() - { - if (!HostHasInternetConnectivity(AF_INET6)) - { - LogSkipped("Host does not have IPv6 internet connectivity. Skipping..."); - return; - } - - // Test AAAA record resolution (IPv6) with both UDP and TCP - VerifyDigDnsResolution(L"dig +short +time=5 AAAA bing.com"); - VerifyDigDnsResolution(L"dig +tcp +short +time=5 AAAA bing.com"); - } - static void VerifyDnsQueries() { // query for A/IPv4 records @@ -5031,42 +5018,6 @@ class VirtioProxyTests VERIFY_IS_TRUE(std::regex_match(out, v6Pattern)); } - TEST_METHOD(DnsResolutionBasicDnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionBasic(); - } - - TEST_METHOD(DnsResolutionDigDnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionDig(); - } - - TEST_METHOD(DnsResolutionRecordTypesDnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionRecordTypes(); - } - - TEST_METHOD(DnsResolutionDigV6DnsTunneling) - { - VIRTIOPROXY_TEST_ONLY(); - DNS_TUNNELING_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = true})); - NetworkTests::VerifyDnsResolutionDigV6(); - } - TEST_METHOD(DnsResolutionBasic) { VIRTIOPROXY_TEST_ONLY(); @@ -5090,13 +5041,5 @@ class VirtioProxyTests m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); NetworkTests::VerifyDnsResolutionRecordTypes(); } - - TEST_METHOD(DnsResolutionDigV6) - { - VIRTIOPROXY_TEST_ONLY(); - - m_config->Update(LxssGenerateTestConfig({.networkingMode = wsl::core::NetworkingMode::VirtioProxy, .dnsTunneling = false})); - NetworkTests::VerifyDnsResolutionDigV6(); - } }; } // namespace NetworkTests