virtiofs: Use aggregate share feature in WSLC (#41151)

* initial work * bump nuget * Address old and new feedback * Use dedicated message for wslc virtiofs mount child * formatting * Last format change --------- Co-authored-by: Daman Mulye <damanmulye@microsoft.com>

Daman Mulye committed Jul 24, 2026 at 18:28 UTC 0f8ad5b3a92450fb9ae88a4ace419772752438a7
16 files changed +247 -146
packages.config
+1 -1
@@ -19,7 +19,7 @@
19 <package id="Microsoft.WSL.bsdtar" version="0.0.2-2" />
20 <package id="Microsoft.WSL.Dependencies.amd64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
21 <package id="Microsoft.WSL.Dependencies.arm64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
22 - <package id="Microsoft.WSL.DeviceHost" version="1.2.53-0" />
22 + <package id="Microsoft.WSL.DeviceHost" version="1.2.54-0" />
23 <package id="Microsoft.WSL.Kernel" version="6.18.35.2-1" targetFramework="native" />
24 <package id="Microsoft.WSL.LinuxSdk" version="1.23.0" targetFramework="native" />
25 <package id="Microsoft.WSL.TestData" version="0.5.0" />
src/linux/init/WSLCInit.cpp
+30 -6
@@ -13,6 +13,7 @@ Abstract:
13 --*/
14
15 #include "util.h"
16 +#include "drvfs.h"
17 #include "SocketChannel.h"
18 #include "message.h"
19 #include "localhost.h"
@@ -668,8 +669,9 @@ void HandleMessageImpl(
669 Transaction.Send(Response);
670 }
671
671 -void HandleMessageImpl(
672 - wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_MOUNT& Message, const gsl::span<gsl::byte>& Buffer)
672 +template <typename TMessage>
673 +void HandleMountMessage(
674 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const TMessage& Message, const gsl::span<gsl::byte>& Buffer)
675 {
676 WSLC_MOUNT_RESULT response{};
677 response.Header.MessageType = WSLC_MOUNT_RESULT::Type;
@@ -686,10 +688,11 @@ void HandleMessageImpl(
688 return "";
689 };
690
689 - mountutil::ParsedOptions options;
691 + const char* mountOptions = readField(Message.OptionsIndex);
692 + mountutil::ParsedOptions options{};
693 if (Message.OptionsIndex > 0)
694 {
692 - options = mountutil::MountParseFlags(wsl::shared::string::FromSpan(Buffer, Message.OptionsIndex));
695 + options = mountutil::MountParseFlags(mountOptions);
696 }
697
698 const char* source = readField(Message.SourceIndex);
@@ -716,7 +719,16 @@ void HandleMessageImpl(
719 THROW_ERRNO_IF(EINVAL, WI_IsFlagSet(Message.Flags, WSLC_MOUNT::Chroot) && !WI_IsFlagSet(Message.Flags, WSLC_MOUNT::OverlayFs));
720
721 auto type = readField(Message.TypeIndex);
719 - THROW_LAST_ERROR_IF(UtilMount(source, target, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < 0);
722 + if constexpr (std::is_same_v<TMessage, WSLC_MOUNT_VIRTIOFS>)
723 + {
724 + const char* childName = readField(Message.ChildNameIndex);
725 + THROW_ERRNO_IF(EINVAL, !wsl::shared::string::IsEqual(type, VIRTIO_FS_TYPE));
726 + THROW_LAST_ERROR_IF(MountVirtioFsChild(source, childName, target, mountOptions) < 0);
727 + }
728 + else
729 + {
730 + THROW_LAST_ERROR_IF(UtilMount(source, target, type, options.MountFlags, options.StringOptions.c_str(), c_defaultRetryTimeout) < 0);
731 + }
732
733 // Workaround for a Linux bug where virtiofs permissions aren't properly propagated when an overlay is mounted on top of a virtiofs share before the permissions have been fetched.
734 // TODO: Remove once fixed upstream.
@@ -818,6 +830,18 @@ void HandleMessageImpl(
830 Transaction.Send<WSLC_MOUNT_RESULT>(response);
831 }
832
833 +void HandleMessageImpl(
834 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_MOUNT& Message, const gsl::span<gsl::byte>& Buffer)
835 +{
836 + HandleMountMessage(Channel, Transaction, Message, Buffer);
837 +}
838 +
839 +void HandleMessageImpl(
840 + wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_MOUNT_VIRTIOFS& Message, const gsl::span<gsl::byte>& Buffer)
841 +{
842 + HandleMountMessage(Channel, Transaction, Message, Buffer);
843 +}
844 +
845 void HandleMessageImpl(
846 wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_EXEC& Message, const gsl::span<gsl::byte>& Buffer)
847 {
@@ -1021,7 +1045,7 @@ void ProcessMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transactio
1045 {
1046 try
1047 {
1024 - HandleMessage<WSLC_GET_DISK, WSLC_MOUNT, WSLC_EXEC, WSLC_FORK, WSLC_CONNECT, WSLC_SIGNAL, WSLC_TTY_RELAY, WSLC_PORT_RELAY, WSLC_UNMOUNT, WSLC_DETACH, WSLC_ACCEPT, WSLC_WATCH_PROCESSES, WSLC_UNIX_CONNECT, WSLC_GET_GUEST_CAPABILITIES, WSLC_LISTDIR>(
1048 + HandleMessage<WSLC_GET_DISK, WSLC_MOUNT, WSLC_MOUNT_VIRTIOFS, WSLC_EXEC, WSLC_FORK, WSLC_CONNECT, WSLC_SIGNAL, WSLC_TTY_RELAY, WSLC_PORT_RELAY, WSLC_UNMOUNT, WSLC_DETACH, WSLC_ACCEPT, WSLC_WATCH_PROCESSES, WSLC_UNIX_CONNECT, WSLC_GET_GUEST_CAPABILITIES, WSLC_LISTDIR>(
1049 Channel, Transaction, Type, Buffer);
1050 }
1051 catch (...)
src/linux/init/drvfs.cpp
+1 -11
@@ -88,13 +88,7 @@ bool IsMountPoint(const std::string& Path)
88 }
89 } // namespace
90
91 -int MountVirtioFsChild(
92 - const char* Tag,
93 - const char* ChildName,
94 - const char* Target,
95 - const char* Options,
96 - int* ExitCode = nullptr,
97 - std::string_view SubPath = "/")
91 +int MountVirtioFsChild(const char* Tag, const char* ChildName, const char* Target, const char* Options, int* ExitCode, std::string_view SubPath)
92 {
93 if ((strcmp(Tag, LX_INIT_DRVFS_VIRTIO_TAG) != 0 && strcmp(Tag, LX_INIT_DRVFS_ADMIN_VIRTIO_TAG) != 0) ||
94 !wsl::shared::string::ToGuid(ChildName) || SubPath.empty() || SubPath.front() != '/')
@@ -902,10 +896,6 @@ try
896 return {};
897 }
898
905 - //
906 - // Validate the tag is a GUID.
907 - //
908 -
899 std::string mappingName{Tag};
900 if (Root != nullptr)
901 {
src/linux/init/drvfs.h
+8
@@ -30,6 +30,14 @@ struct VirtioFsMountRoot
30
31 std::optional<VirtioFsMountRoot> ParseAggregateVirtioFsMountRoot(std::string_view Tag, std::string_view Root);
32
33 +int MountVirtioFsChild(
34 + const char* Tag,
35 + const char* ChildName,
36 + const char* Target,
37 + const char* Options,
38 + int* ExitCode = nullptr,
39 + std::string_view SubPath = "/");
40 +
41 int MountDrvfs(const char* Source, const char* Target, const char* Options, std::optional<bool> Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode = nullptr);
42
43 int MountDrvfsEntry(int Argc, char* Argv[]);
src/linux/init/util.cpp
+6 -6
@@ -878,16 +878,16 @@ try
878 while (MountEnum.Next())
879 {
880 //
881 - // Skip internal virtiofs device mounts. The aggregate virtiofs root and
882 - // its per-share child binds live under VIRTIOFS_MOUNT_DIR and carry the
883 - // same Windows source as the user-facing /mnt/<drive> bind mounts. If
884 - // they were considered, reverse (Windows->Linux) translation could
885 - // return an internal plumbing path (for example
881 + // When translating Windows paths to Linux, skip internal virtiofs
882 + // device mounts. The aggregate virtiofs root and its per-share child
883 + // binds live under VIRTIOFS_MOUNT_DIR and carry the same Windows source
884 + // as the user-facing /mnt/<drive> bind mounts. If they were considered,
885 + // translation could return an internal plumbing path (for example
886 // /run/wsl/virtiofs-mounts/drvfsa/<guid>) instead of the real mount
887 // point such as /mnt/c.
888 //
889
890 - if (UtilIsPathPrefix(MountEnum.Current().MountPoint, VIRTIOFS_MOUNT_DIR, false) > 0)
890 + if (WinPath && UtilIsPathPrefix(MountEnum.Current().MountPoint, VIRTIOFS_MOUNT_DIR, false) > 0)
891 {
892 continue;
893 }
src/shared/inc/defs.h
-5
@@ -52,11 +52,6 @@ inline constexpr std::uint32_t VersionMinor = WSL_PACKAGE_VERSION_MINOR;
52 inline constexpr std::uint32_t VersionRevision = WSL_PACKAGE_VERSION_REVISION;
53 inline constexpr std::tuple<uint32_t, uint32_t, uint32_t> PackageVersion{VersionMajor, VersionMinor, VersionRevision};
54
55 -// Maximum number of virtiofs shares that can be mounted (with different paths) over the lifetime of a VM.
56 -// This limit is there to avoid a hang when too many virtiofs shares are mounted.
57 -// TODO: Remove once we can use the same PCI devices for all shares.
58 -inline constexpr size_t c_maxVirtioFsShares = 15;
59 -
55 #ifdef WSL_OFFICIAL_BUILD
56
57 inline constexpr bool OfficialBuild = true;
src/shared/inc/lxinitshared.h
+21
@@ -413,6 +413,7 @@ typedef enum _LX_MESSAGE_TYPE
413 LxMessageWSLCGetGuestCapabilitiesResult,
414 LxMessageWSLCListDir,
415 LxMessageWSLCListDirResult,
416 + LxMessageWSLCMountVirtioFs,
417 } LX_MESSAGE_TYPE,
418 *PLX_MESSAGE_TYPE;
419
@@ -527,6 +528,7 @@ inline auto ToString(LX_MESSAGE_TYPE messageType)
528 X(LxMessageWSLCGetGuestCapabilitiesResult)
529 X(LxMessageWSLCListDir)
530 X(LxMessageWSLCListDirResult)
531 + X(LxMessageWSLCMountVirtioFs)
532
533 default:
534 return "<unexpected LX_MESSAGE_TYPE>";
@@ -1657,6 +1659,25 @@ struct WSLC_MOUNT
1659 PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex), STRING_FIELD(DestinationIndex), STRING_FIELD(TypeIndex), STRING_FIELD(OptionsIndex));
1660 };
1661
1662 +struct WSLC_MOUNT_VIRTIOFS
1663 +{
1664 + static inline auto Type = LxMessageWSLCMountVirtioFs;
1665 + using TResponse = WSLC_MOUNT_RESULT;
1666 +
1667 + DECLARE_MESSAGE_CTOR(WSLC_MOUNT_VIRTIOFS);
1668 +
1669 + MESSAGE_HEADER Header{};
1670 + unsigned int SourceIndex{};
1671 + unsigned int DestinationIndex{};
1672 + unsigned int TypeIndex{};
1673 + unsigned int OptionsIndex{};
1674 + unsigned int Flags{};
1675 + unsigned int ChildNameIndex{};
1676 + char Buffer[];
1677 +
1678 + PRETTY_PRINT(FIELD(Header), STRING_FIELD(SourceIndex), STRING_FIELD(DestinationIndex), STRING_FIELD(TypeIndex), STRING_FIELD(OptionsIndex), STRING_FIELD(ChildNameIndex));
1679 +};
1680 +
1681 struct WSLC_EXEC
1682 {
1683 static inline auto Type = LxMessageWSLCExec;
src/windows/common/DeviceHostProxy.cpp
+8
@@ -180,6 +180,14 @@ void DeviceHostProxy::AddVirtiofsChild(const GUID& InstanceId, const std::wstrin
180 THROW_IF_FAILED(GetVirtiofsDevice(InstanceId)->AddChild(name.get(), rootPath.get(), mountOptions.get()));
181 }
182
183 +void DeviceHostProxy::RemoveVirtiofsChild(const GUID& InstanceId, const std::wstring& Name)
184 +{
185 + std::lock_guard lifecycleLock(m_deviceLifecycleLock);
186 +
187 + const auto name = wil::make_bstr(Name.c_str());
188 + THROW_IF_FAILED(GetVirtiofsDevice(InstanceId)->RemoveChild(name.get()));
189 +}
190 +
191 GUID DeviceHostProxy::AddVirtioPmemDevice(_In_ HANDLE UserToken, const std::wstring& Path, bool Writable)
192 {
193 std::lock_guard lifecycleLock(m_deviceLifecycleLock);
src/windows/common/DeviceHostProxy.h
+2
@@ -23,6 +23,8 @@ public:
23
24 void AddVirtiofsChild(const GUID& InstanceId, const std::wstring& Name, const std::wstring& RootPath, const std::wstring& MountOptions);
25
26 + void RemoveVirtiofsChild(const GUID& InstanceId, const std::wstring& Name);
27 +
28 GUID AddVirtioPmemDevice(_In_ HANDLE UserToken, const std::wstring& Path, bool Writable);
29
30 void RemoveDevice(const GUID& InstanceId);
src/windows/common/GuestDeviceManager.cpp
+7
@@ -33,6 +33,13 @@ void GuestDeviceManager::AddVirtiofsChild(_In_ const GUID& InstanceId, _In_ PCWS
33 m_deviceHostSupport->AddVirtiofsChild(InstanceId, Name, RootPath, MountOptions ? MountOptions : L"");
34 }
35
36 +_Requires_lock_not_held_(m_lock)
37 +void GuestDeviceManager::RemoveVirtiofsChild(_In_ const GUID& InstanceId, _In_ PCWSTR Name)
38 +{
39 + auto guestDeviceLock = m_lock.lock_exclusive();
40 + m_deviceHostSupport->RemoveVirtiofsChild(InstanceId, Name);
41 +}
42 +
43 _Requires_lock_not_held_(m_lock)
44 GUID GuestDeviceManager::AddVirtioPmemDevice(_In_ PCWSTR Path, bool ReadOnly, _In_ HANDLE UserToken)
45 {
src/windows/common/GuestDeviceManager.h
+3
@@ -27,6 +27,9 @@ public:
27 _Requires_lock_not_held_(m_lock)
28 void AddVirtiofsChild(_In_ const GUID& InstanceId, _In_ PCWSTR Name, _In_opt_ PCWSTR MountOptions, _In_ PCWSTR RootPath);
29
30 + _Requires_lock_not_held_(m_lock)
31 + void RemoveVirtiofsChild(_In_ const GUID& InstanceId, _In_ PCWSTR Name);
32 +
33 _Requires_lock_not_held_(m_lock)
34 GUID AddVirtioPmemDevice(_In_ PCWSTR Path, bool ReadOnly, _In_ HANDLE UserToken);
35
src/windows/service/exe/HcsVirtualMachine.cpp
+11 -2
@@ -620,7 +620,15 @@ try
620 {
621 std::wstring options = ReadOnly ? L"ro" : L"";
622
623 - it->second = m_guestDeviceManager->AddVirtiofsDevice(shareName.c_str(), options.c_str(), WindowsPath, m_userToken.get());
623 + if (!m_virtioFsDevice.has_value())
624 + {
625 + VirtioFsShareOptions aggregateOptions{.Kind = VirtiofsShareKind_Aggregate};
626 + m_virtioFsDevice =
627 + m_guestDeviceManager->AddVirtiofsDevice(TEXT(LX_INIT_DRVFS_VIRTIO_TAG), L"", L"", m_userToken.get(), aggregateOptions);
628 + }
629 +
630 + m_guestDeviceManager->AddVirtiofsChild(m_virtioFsDevice.value(), shareName.c_str(), options.c_str(), WindowsPath);
631 + it->second = m_virtioFsDevice;
632 }
633
634 cleanup.release();
@@ -645,7 +653,8 @@ try
653 }
654 else
655 {
648 - m_guestDeviceManager->RemoveGuestDevice(it->second.value());
656 + auto shareName = wsl::shared::string::GuidToString<wchar_t>(it->first, wsl::shared::string::None);
657 + m_guestDeviceManager->RemoveVirtiofsChild(it->second.value(), shareName.c_str());
658 }
659
660 m_shares.erase(it);
src/windows/service/exe/HcsVirtualMachine.h
+2 -1
@@ -97,8 +97,9 @@ private:
97 std::map<ULONG, DiskInfo> m_attachedDisks;
98 std::bitset<MAX_VHD_COUNT> m_lunBitmap;
99
100 - // Shares: key is ShareId, value is nullopt for Plan9 or DeviceInstanceId for VirtioFS
100 + // Shares: key is ShareId, value is nullopt for Plan9 or the aggregate DeviceInstanceId for VirtioFS.
101 std::map<GUID, std::optional<GUID>, wsl::windows::common::helpers::GuidLess> m_shares;
102 + std::optional<GUID> m_virtioFsDevice;
103
104 std::filesystem::path m_vmSavedStateFile;
105 std::filesystem::path m_crashDumpFolder;
src/windows/wslcsession/WSLCVirtualMachine.cpp
+30 -43
@@ -886,6 +886,30 @@ void WSLCVirtualMachine::Mount(shared::SocketChannel& Channel, LPCSTR Source, LP
886 THROW_HR_IF(E_FAIL, response.Result != 0);
887 }
888
889 +void WSLCVirtualMachine::MountVirtioFsChild(shared::SocketChannel& Channel, LPCSTR Source, LPCSTR ChildName, LPCSTR Target, LPCSTR Options, ULONG Flags)
890 +{
891 + wsl::shared::MessageWriter<WSLC_MOUNT_VIRTIOFS> message;
892 + message.WriteString(message->SourceIndex, Source);
893 + message.WriteString(message->ChildNameIndex, ChildName);
894 + message.WriteString(message->DestinationIndex, Target);
895 + message.WriteString(message->TypeIndex, "virtiofs");
896 + message.WriteString(message->OptionsIndex, Options);
897 + message->Flags = Flags;
898 +
899 + const auto& response = Channel.Transaction<WSLC_MOUNT_VIRTIOFS>(message.Span());
900 +
901 + WSL_LOG(
902 + "WSLCMountVirtioFsChild",
903 + TraceLoggingValue(Source, "Source"),
904 + TraceLoggingValue(ChildName, "ChildName"),
905 + TraceLoggingValue(Target, "Target"),
906 + TraceLoggingValue(Options, "Options"),
907 + TraceLoggingValue(Flags, "Flags"),
908 + TraceLoggingValue(response.Result, "Result"));
909 +
910 + THROW_HR_IF(E_FAIL, response.Result != 0);
911 +}
912 +
913 int32_t WSLCVirtualMachine::ExpectClosedChannelOrError(wsl::shared::SocketChannel& Channel)
914 {
915 auto [response, span] = Channel.ReceiveMessageOrClosed<RESULT_MESSAGE<int32_t>>();
@@ -1077,9 +1101,7 @@ try
1101 THROW_HR_IF_MSG(E_INVALIDARG, LinuxPath[0] != '/', "Mountpoint is not absolute: '%hs'", LinuxPath);
1102
1103 const bool readOnly = WI_IsFlagSet(Flags, WSLCMountFlagsReadOnly);
1080 - auto normalizedPath = std::filesystem::weakly_canonical(path).wstring();
1104 GUID shareGuid{};
1082 - bool reusingShare = false;
1105
1106 {
1107 std::lock_guard lock(m_lock);
@@ -1088,34 +1110,8 @@ try
1110 auto it = m_mountedWindowsFolders.find(LinuxPath);
1111 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), it != m_mountedWindowsFolders.end());
1112
1091 - // In VirtioFs mode, try to reuse an existing share for the same Windows path and access mode.
1092 - if (FeatureEnabled(WslcFeatureFlagsVirtioFs))
1093 - {
1094 - auto shareIt = m_virtioFsShares.find({normalizedPath, readOnly});
1095 - if (shareIt != m_virtioFsShares.end())
1096 - {
1097 - shareGuid = shareIt->second;
1098 - reusingShare = true;
1099 - }
1100 - else
1101 - {
1102 - THROW_HR_WITH_USER_ERROR_IF(
1103 - E_OUTOFMEMORY,
1104 - shared::Localization::MessageWslcTooManyVirtioFsShares(shared::c_maxVirtioFsShares),
1105 - m_virtioFsShares.size() >= shared::c_maxVirtioFsShares);
1106 - }
1107 - }
1108 -
1109 - if (!reusingShare)
1110 - {
1111 - // Delegate to IWSLCVirtualMachine for the privileged share creation
1112 - THROW_IF_FAILED(m_vm->AddShare(WindowsPath, readOnly, &shareGuid));
1113 -
1114 - if (FeatureEnabled(WslcFeatureFlagsVirtioFs))
1115 - {
1116 - m_virtioFsShares[{normalizedPath, readOnly}] = shareGuid;
1117 - }
1118 - }
1113 + // Delegate to IWSLCVirtualMachine for the privileged share creation.
1114 + THROW_IF_FAILED(m_vm->AddShare(WindowsPath, readOnly, &shareGuid));
1115
1116 m_mountedWindowsFolders.emplace(LinuxPath, shareGuid);
1117 }
@@ -1126,11 +1122,7 @@ try
1122 if (WI_VERIFY(mountIt != m_mountedWindowsFolders.end()))
1123 {
1124 m_mountedWindowsFolders.erase(mountIt);
1129 - if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
1130 - {
1131 - m_virtioFsShares.erase({normalizedPath, readOnly});
1132 - LOG_IF_FAILED(m_vm->RemoveShare(shareGuid));
1133 - }
1125 + LOG_IF_FAILED(m_vm->RemoveShare(shareGuid));
1126 }
1127 });
1128
@@ -1154,7 +1146,7 @@ try
1146 else
1147 {
1148 std::string options = readOnly ? "ro" : "rw";
1157 - Mount(m_initChannel, shareName.c_str(), LinuxPath, "virtiofs", options.c_str(), Flags);
1149 + MountVirtioFsChild(m_initChannel, LX_INIT_DRVFS_VIRTIO_TAG, shareName.c_str(), LinuxPath, options.c_str(), Flags);
1150 }
1151
1152 deleteOnFailure.release();
@@ -1178,13 +1170,8 @@ try
1170
1171 auto shareId = it->second;
1172
1181 - // Keep the share mounted in virtiofs mode to avoid accumulating devices, which can cause a hang when reached.
1182 - // TODO: Actually remove the device once this is supported by the device host.
1183 - if (!FeatureEnabled(WslcFeatureFlagsVirtioFs))
1184 - {
1185 - // Delegate to IWSLCVirtualMachine for the privileged share removal
1186 - THROW_IF_FAILED(m_vm->RemoveShare(shareId));
1187 - }
1173 + // Delegate to IWSLCVirtualMachine for the privileged share removal.
1174 + THROW_IF_FAILED(m_vm->RemoveShare(shareId));
1175
1176 m_mountedWindowsFolders.erase(it);
1177
src/windows/wslcsession/WSLCVirtualMachine.h
+2 -4
@@ -204,6 +204,8 @@ private:
204 void ReadGuestCapabilities();
205
206 static void Mount(wsl::shared::SocketChannel& Channel, LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags);
207 + static void MountVirtioFsChild(
208 + wsl::shared::SocketChannel& Channel, _In_ LPCSTR Source, _In_ LPCSTR ChildName, _In_ LPCSTR Target, _In_ LPCSTR Options, _In_ ULONG Flags);
209 void MountGpuLibraries(_In_ LPCSTR LibrariesMountPoint, _In_ LPCSTR DriversMountpoint);
210
211 Microsoft::WRL::ComPtr<WSLCProcess> CreateLinuxProcessImpl(
@@ -280,10 +282,6 @@ private:
282 std::map<ULONG, AttachedDisk> m_attachedDisks;
283 std::map<std::string, GUID> m_mountedWindowsFolders;
284
283 - // VirtioFs share cache: maps (normalized WindowsPath, readOnly) to share GUID.
284 - // Shares are kept alive after unmount for reuse on subsequent mounts of the same folder.
285 - std::map<std::pair<std::wstring, bool>, GUID> m_virtioFsShares;
286 -
285 std::recursive_mutex m_lock;
286 std::mutex m_portRelaylock;
287 };
test/windows/WSLCTests.cpp
+115 -67
@@ -3763,19 +3763,20 @@ class WSLCTests
3763 VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3764 ExpectMount(session.get(), "/win-path", expectedMountOptions(true));
3765
3766 - // Capture the mount source and type, unmount, then remount without read-only.
3766 + // Remount a bind of the share as read-write to ensure the device host still enforces read-only access.
3767 ExpectCommandResult(
3768 session.get(),
3769 {"/bin/sh",
3770 "-c",
3771 - "src=$(findmnt -n -o SOURCE /win-path) && "
3772 - "fstype=$(findmnt -n -o FSTYPE /win-path) && "
3773 - "umount /win-path && "
3774 - "mount -t $fstype $src /win-path"},
3771 + "mkdir -p /win-path-rw && "
3772 + "mount --bind /win-path /win-path-rw && "
3773 + "mount -o remount,bind,rw /win-path-rw && "
3774 + "findmnt -n -o VFS-OPTIONS /win-path-rw | grep -qE '(^|,)rw(,|$)'"},
3775 0);
3776
3777 - // Verify the folder is still not writeable.
3778 - ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path/file.txt"}, 1);
3777 + // Verify the folder is still not writeable through the read-write bind.
3778 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "echo -n content > /win-path-rw/file.txt"}, 1);
3779 + ExpectCommandResult(session.get(), {"/bin/sh", "-c", "umount /win-path-rw && rmdir /win-path-rw"}, 0);
3780
3781 VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3782 ExpectMount(session.get(), "/win-path", {});
@@ -3809,107 +3810,154 @@ class WSLCTests
3810 ValidateWindowsMounts(true);
3811 }
3812
3812 - // Validates that VirtioFs shares are reused across mount/unmount cycles for the same Windows folder.
3813 - WSLC_TEST_METHOD(WindowsMountsVirtioFsShareReuse)
3813 + // Validates that each mount owns an independent child on the shared aggregate device.
3814 + WSLC_TEST_METHOD(WindowsMountsVirtioFsIndependentShares)
3815 {
3815 - auto settings = GetDefaultSessionSettings(L"virtiofs-share-reuse-test");
3816 + auto settings = GetDefaultSessionSettings(L"virtiofs-independent-shares-test");
3817 WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3818
3819 auto createNewSession = !WI_IsFlagSet(m_defaultSessionSettings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3820 auto session = createNewSession ? CreateSession(settings) : m_defaultSession;
3821
3821 - auto testFolder = std::filesystem::current_path() / "test-folder-share-reuse";
3822 + auto testFolder = std::filesystem::current_path() / "test-folder-independent-shares";
3823 std::filesystem::create_directories(testFolder);
3824 + std::ofstream(testFolder / "marker.txt") << "content";
3825 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testFolder); });
3826
3825 - auto getMountSource = [&](const char* mountPoint) -> std::string {
3826 - auto cmd = std::format("findmnt -n -o SOURCE {}", mountPoint);
3827 + auto getMountField = [&](const char* mountPoint, const char* field) -> std::string {
3828 + auto cmd = std::format("findmnt -n -o {} {}", field, mountPoint);
3829 auto result = ExpectCommandResult(session.get(), {"/bin/sh", "-c", cmd}, 0);
3830 return result.Output[1];
3831 };
3832
3831 - // Mount, capture the source (share GUID), unmount, remount, verify same GUID is reused.
3833 + // Concurrent mounts of the same host path use distinct children on the same aggregate device.
3834 {
3833 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3834 - auto firstSource = getMountSource("/win-path");
3835 - VERIFY_IS_FALSE(firstSource.empty());
3835 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-1", false));
3836 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-2", false));
3837
3837 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3838 - ExpectMount(session.get(), "/win-path", {});
3838 + auto firstDevice = getMountField("/win-path-1", "MAJ:MIN");
3839 + auto secondDevice = getMountField("/win-path-2", "MAJ:MIN");
3840 + auto firstRoot = getMountField("/win-path-1", "FSROOT");
3841 + auto secondRoot = getMountField("/win-path-2", "FSROOT");
3842 + VERIFY_ARE_EQUAL(firstDevice, secondDevice);
3843 + VERIFY_ARE_NOT_EQUAL(firstRoot, secondRoot);
3844 + VERIFY_IS_TRUE(firstRoot.starts_with('/'));
3845 + VERIFY_IS_TRUE(firstRoot.ends_with('\n'));
3846 + VERIFY_IS_TRUE(secondRoot.starts_with('/'));
3847 + VERIFY_IS_TRUE(secondRoot.ends_with('\n'));
3848 + firstRoot.pop_back();
3849 + secondRoot.pop_back();
3850
3840 - // Remount the same folder - should reuse the same share GUID.
3841 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3842 - auto secondSource = getMountSource("/win-path");
3851 + const auto firstChild = std::format("/run/wsl/virtiofs-mounts/{}{}", LX_INIT_DRVFS_VIRTIO_TAG, firstRoot);
3852 + const auto secondChild = std::format("/run/wsl/virtiofs-mounts/{}{}", LX_INIT_DRVFS_VIRTIO_TAG, secondRoot);
3853
3844 - VERIFY_ARE_EQUAL(firstSource, secondSource);
3854 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-1"));
3855 + ExpectCommandResult(session.get(), {"/bin/cat", "/win-path-2/marker.txt"}, 0);
3856 + ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", firstChild}, 0);
3857 + ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", secondChild}, 0);
3858
3846 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3859 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-2"));
3860 + ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", secondChild}, 0);
3861 }
3862
3849 - // Verify that changing the read-only flag produces a different share GUID.
3863 + // Verify that read-write and read-only shares use different children on the same aggregate device.
3864 {
3851 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", false));
3852 - auto rwSource = getMountSource("/win-path");
3865 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-rw", false));
3866 + VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path-ro", true));
3867
3854 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3868 + auto rwDevice = getMountField("/win-path-rw", "MAJ:MIN");
3869 + auto roDevice = getMountField("/win-path-ro", "MAJ:MIN");
3870 + auto rwRoot = getMountField("/win-path-rw", "FSROOT");
3871 + auto roRoot = getMountField("/win-path-ro", "FSROOT");
3872
3856 - VERIFY_SUCCEEDED(session->MountWindowsFolder(testFolder.c_str(), "/win-path", true));
3857 - auto roSource = getMountSource("/win-path");
3873 + VERIFY_ARE_EQUAL(rwDevice, roDevice);
3874 + VERIFY_ARE_NOT_EQUAL(rwRoot, roRoot);
3875
3859 - VERIFY_ARE_NOT_EQUAL(rwSource, roSource);
3860 -
3861 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path"));
3876 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-rw"));
3877 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/win-path-ro"));
3878 }
3879 }
3880
3865 - // Validate that the correct error is returned when too many virtiofs shares are mounted.
3866 - WSLC_TEST_METHOD(VirtiofsVolumesLimit)
3881 + WSLC_TEST_METHOD(WindowsMountsVirtioFsRemoveChild)
3882 {
3868 - constexpr size_t c_maxVirtioFsShares = wsl::shared::c_maxVirtioFsShares;
3883 + auto settings = GetDefaultSessionSettings(L"virtiofs-remove-child-test");
3884 + WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3885 + auto session = CreateSession(settings);
3886 +
3887 + const auto testRoot = std::filesystem::current_path() / "test-folder-remove-child";
3888 + const auto firstFolder = testRoot / "first";
3889 + const auto secondFolder = testRoot / "second";
3890 + std::filesystem::create_directories(firstFolder);
3891 + std::filesystem::create_directories(secondFolder);
3892 + std::ofstream(firstFolder / "marker.txt") << "first";
3893 + std::ofstream(secondFolder / "marker.txt") << "second";
3894 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testRoot); });
3895 +
3896 + auto getMountRoot = [&](const char* mountPoint) {
3897 + const auto command = std::format("findmnt -n -o FSROOT {}", mountPoint);
3898 + auto root = ExpectCommandResult(session.get(), {"/bin/sh", "-c", command}, 0).Output.at(1);
3899 + VERIFY_IS_TRUE(root.starts_with('/'));
3900 + VERIFY_IS_TRUE(root.ends_with('\n'));
3901 + root.pop_back();
3902 + return root;
3903 + };
3904 +
3905 + VERIFY_SUCCEEDED(session->MountWindowsFolder(firstFolder.c_str(), "/remove-child-first", false));
3906 + VERIFY_SUCCEEDED(session->MountWindowsFolder(secondFolder.c_str(), "/remove-child-second", false));
3907
3870 - auto settings = GetDefaultSessionSettings(L"virtiofs-share-limit-test");
3908 + const auto firstRoot = getMountRoot("/remove-child-first");
3909 + const auto secondRoot = getMountRoot("/remove-child-second");
3910 + VERIFY_ARE_NOT_EQUAL(firstRoot, secondRoot);
3911 +
3912 + const auto aggregateRoot = std::format("/run/wsl/virtiofs-mounts/{}", LX_INIT_DRVFS_VIRTIO_TAG);
3913 + const auto firstChild = aggregateRoot + firstRoot;
3914 + const auto secondChild = aggregateRoot + secondRoot;
3915 + ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", firstChild}, 0);
3916 + ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", secondChild}, 0);
3917 +
3918 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/remove-child-first"));
3919 + ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", firstChild}, 0);
3920 + ExpectCommandResult(session.get(), {"/bin/cat", "/remove-child-second/marker.txt"}, 0);
3921 + ExpectCommandResult(session.get(), {"/usr/bin/test", "-e", secondChild}, 0);
3922 +
3923 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/remove-child-second"));
3924 + ExpectCommandResult(session.get(), {"/usr/bin/test", "!", "-e", secondChild}, 0);
3925 + }
3926 +
3927 + // Validate that enough VirtioFs shares can be mounted to exceed the old per-device aperture limit.
3928 + WSLC_TEST_METHOD(VirtiofsMountManyVolumes)
3929 + {
3930 + constexpr size_t c_shareCount = 32;
3931 +
3932 + auto settings = GetDefaultSessionSettings(L"virtiofs-many-shares-test");
3933 WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsVirtioFs);
3934
3873 - // Use a dedicated session so the share count starts at zero (no GPU libraries are mounted).
3935 auto session = CreateSession(settings);
3936
3876 - auto testRoot = std::filesystem::current_path() / "test-folder-share-limit";
3937 + auto testRoot = std::filesystem::current_path() / "test-folder-many-shares";
3938 std::filesystem::create_directories(testRoot);
3939 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { std::filesystem::remove_all(testRoot); });
3940 + std::vector<std::string> mountPoints;
3941
3880 - auto folderForIndex = [&](size_t index) {
3881 - auto folder = testRoot / std::to_string(index);
3882 - std::filesystem::create_directories(folder);
3883 - return folder;
3884 - };
3885 -
3886 - // Mount distinct Windows folders (each creates a new share) until the limit is reached.
3887 - size_t mounted = 0;
3888 - HRESULT lastResult = S_OK;
3889 - for (size_t i = 0; i <= c_maxVirtioFsShares; ++i)
3942 + for (size_t index = 0; index < c_shareCount; ++index)
3943 {
3891 - auto folder = folderForIndex(i);
3892 - auto mountPoint = std::format("/vfs-limit-{}", i);
3944 + const auto folder = testRoot / std::to_string(index);
3945 + std::filesystem::create_directories(folder);
3946 + std::ofstream(folder / "marker.txt") << index;
3947
3894 - lastResult = session->MountWindowsFolder(folder.c_str(), mountPoint.c_str(), false);
3895 - if (FAILED(lastResult))
3896 - {
3897 - break;
3898 - }
3948 + const auto mountPoint = std::format("/vfs-many-{}", index);
3949 + VERIFY_SUCCEEDED(session->MountWindowsFolder(folder.c_str(), mountPoint.c_str(), false));
3950 + mountPoints.emplace_back(mountPoint);
3951
3900 - mounted++;
3952 + const auto command = std::format("cat {}/marker.txt", mountPoint);
3953 + const auto result = ExpectCommandResult(session.get(), {"/bin/sh", "-c", command}, 0);
3954 + VERIFY_ARE_EQUAL(std::to_string(index), result.Output.at(1));
3955 }
3956
3903 - VERIFY_ARE_EQUAL(mounted, c_maxVirtioFsShares);
3904 - VERIFY_ARE_EQUAL(lastResult, E_OUTOFMEMORY);
3905 - ValidateCOMErrorMessage(
3906 - L"Too many volumes have been mounted (limit: 15). Restart the session to mount more volumes. This will be fixed in a "
3907 - L"future release.");
3908 -
3909 - // Reusing an already-created share must still succeed.
3910 - auto reusedFolder = folderForIndex(0);
3911 - VERIFY_SUCCEEDED(session->MountWindowsFolder(reusedFolder.c_str(), "/vfs-limit-reuse", false));
3912 - VERIFY_SUCCEEDED(session->UnmountWindowsFolder("/vfs-limit-reuse"));
3957 + for (const auto& mountPoint : mountPoints)
3958 + {
3959 + VERIFY_SUCCEEDED(session->UnmountWindowsFolder(mountPoint.c_str()));
3960 + }
3961 }
3962
3963 // This test case validates that no file descriptors are leaked to user processes.