diff --git a/doc/docs/technical-documentation/drvfs.md b/doc/docs/technical-documentation/drvfs.md index 6f6cd0095..647390d5b 100644 --- a/doc/docs/technical-documentation/drvfs.md +++ b/doc/docs/technical-documentation/drvfs.md @@ -32,4 +32,32 @@ mount -t drvfs C: /tmp/my-mount-point Internally, this is handled by `/usr/sbin/mount.drvfs`, which is a symlink to `/init`. When `/init` starts, it looks at `argv[0]` to determine which entrypoint to run. If `argv[0]` is `mount.drvfs`, then `/init` runs the `mount.drvfs` entrypoint (see `MountDrvfsEntry()` in `src/linux/init/drvfs.cpp`). -Depending on the distribution configuration, `mount.drvfs` will either mount the drive as `drvfs` (WSL1), or `plan9`, `virtio-plan9` or `virtiofs` (WSL), depending on [.wslconfig](https://learn.microsoft.com/windows/wsl/wsl-config). \ No newline at end of file +Depending on the distribution configuration, `mount.drvfs` will either mount the drive as `drvfs` (WSL1), or `plan9`, `virtio-plan9` or `virtiofs` (WSL), depending on [.wslconfig](https://learn.microsoft.com/windows/wsl/wsl-config). + +## Mounting with an explicit transport + +`mount.drvfs` accepts a `transport=` mount option that overrides the default transport selected by `.wslconfig`: + +```bash +sudo mount -t drvfs C: /mnt/c_plan9 -o transport=plan9 +sudo mount -t drvfs C: /mnt/c_virtio9p -o transport=virtio9p +sudo mount -t drvfs C: /mnt/c_virtiofs -o transport=virtiofs +``` + +Accepted values are `plan9`, `virtio9p`, and `virtiofs`. Unknown or empty values cause the mount to fail with an error. + +The requested transport must be available in the VM. By default the host only stands up the transport selected by `wsl2.virtio9p` / `wsl2.virtiofs`, so mounting with a `transport=` value whose backend is not running fails at mount time. To make all three transports available simultaneously in a single VM (useful for development and benchmarking), enable the experimental setting below. + +## Testing all three transports in a single VM (experimental) + +To exercise the three DrvFs transports concurrently in a single VM, enable: + +```ini +# .wslconfig +[experimental] +drvFsTransports = true +``` + +When this setting is enabled, the host stands up all three transport backends (plan9-over-hvsocket server, virtio9p server, and virtiofs worker) regardless of `wsl2.virtio9p` / `wsl2.virtiofs`. Combined with the `transport=` mount option above, this lets the same drive be mounted under each transport in the same VM without restarting. + +This setting is intended for diagnostics and is off by default; it has a small steady-state cost (extra plan9 server and virtiofs worker thread) when enabled. diff --git a/src/linux/init/config.cpp b/src/linux/init/config.cpp index 3d3b59bfd..9d8c458fb 100644 --- a/src/linux/init/config.cpp +++ b/src/linux/init/config.cpp @@ -2404,13 +2404,41 @@ try volumesToMount[driveIndex.value()] = false; } + // + // Detect the transport used by the existing mount from its + // super options. virtio9p mounts have `trans=virtio`; hvsocket + // plan9 mounts have `trans=fd`. This is required because the + // global default (WSL_USE_VIRTIO_9P) may not match the actual + // transport when the experimental DrvFsTransports option lets + // mounts use different transports concurrently. + // + + DrvFsTransport ExistingTransport = WSL_USE_VIRTIO_9P() ? DrvFsTransport::Virtio9p : DrvFsTransport::Plan9; + { + std::string_view SuperOptions = MountEntry.SuperOptions; + while (!SuperOptions.empty()) + { + auto Option = UtilStringNextToken(SuperOptions, ","); + if (Option == "trans=virtio") + { + ExistingTransport = DrvFsTransport::Virtio9p; + break; + } + else if (Option == "trans=fd") + { + ExistingTransport = DrvFsTransport::Plan9; + break; + } + } + } + // // Construct new Plan9 mount options based on the existing mount. // NewMountOptions = MountEntry.MountOptions; NewMountOptions += ','; - if (WSL_USE_VIRTIO_9P()) + if (ExistingTransport == DrvFsTransport::Virtio9p) { // // Check if the existing mount is a drvfs mount that needs to be remounted. @@ -2443,7 +2471,7 @@ try NewMountOptions += ','; } - MountPlan9Share(NewSource, MountEntry.MountPoint, NewMountOptions.c_str(), Message->Admin); + MountPlan9Share(NewSource, MountEntry.MountPoint, NewMountOptions.c_str(), Message->Admin, ExistingTransport); } else if (strcmp(MountEntry.FileSystemType, VIRTIO_FS_TYPE) == 0) { diff --git a/src/linux/init/drvfs.cpp b/src/linux/init/drvfs.cpp index 128157e57..6374ba655 100644 --- a/src/linux/init/drvfs.cpp +++ b/src/linux/init/drvfs.cpp @@ -156,10 +156,124 @@ Return Value: return {std::move(Plan9Options), std::move(StandardOptions)}; } -bool IsDrvfsElevated(void) +DrvFsTransport ParseTransportName(std::string_view Value) + +/*++ + +Routine Description: + + Parses a transport= option value into a DrvFsTransport enum. + +Arguments: + + Value - Supplies the value portion of a transport= option. + +Return Value: + + The parsed transport. Unknown values log a warning and return Invalid. + +--*/ + +{ + if (Value == "plan9") + { + return DrvFsTransport::Plan9; + } + else if (Value == "virtio9p") + { + return DrvFsTransport::Virtio9p; + } + else if (Value == "virtiofs") + { + return DrvFsTransport::Virtiofs; + } + + LOG_WARNING("Unknown DrvFs transport '{}'", Value); + return DrvFsTransport::Invalid; +} + +DrvFsTransport ExtractTransportOption(std::string& Options) /*++ +Routine Description: + + Scans an option string for one or more transport= tokens, removes + each from Options, and returns the requested transport. If multiple + transport= tokens are present, the last one wins. If no transport= + token is found, the function returns DrvFsTransport::Default and Options + is left unchanged. If the value is empty or unrecognized, returns + DrvFsTransport::Invalid. + +Arguments: + + Options - Supplies a comma-separated DrvFs option string. Modified in + place: any transport= tokens are removed. + +Return Value: + + The requested transport, DrvFsTransport::Default, or DrvFsTransport::Invalid. + +--*/ + +{ + constexpr std::string_view Prefix = "transport="; + + if (Options.empty()) + { + return DrvFsTransport::Default; + } + + DrvFsTransport Result = DrvFsTransport::Default; + std::string Rebuilt; + Rebuilt.reserve(Options.size()); + bool Found = false; + + std::string_view Remaining = Options; + bool First = true; + while (!Remaining.empty()) + { + auto Token = UtilStringNextToken(Remaining, ","); + if (wsl::shared::string::StartsWith(Token, Prefix)) + { + Found = true; + const auto Value = std::string_view{Token}.substr(Prefix.size()); + if (Value.empty()) + { + LOG_WARNING("transport= option requires a value (plan9, virtio9p, virtiofs)"); + Result = DrvFsTransport::Invalid; + continue; + } + + if (Result != DrvFsTransport::Default) + { + LOG_WARNING("Multiple transport= options, using last"); + } + + Result = ParseTransportName(Value); + continue; + } + + if (!First) + { + Rebuilt += ','; + } + + Rebuilt += Token; + First = false; + } + + if (Found) + { + Options = std::move(Rebuilt); + } + + return Result; +} + +bool IsDrvfsElevated(void) +/*++ + Routine Description: This routine determines whether drvfs mounts should use the elevated server. @@ -354,16 +468,58 @@ Return Value: try { - if (!UtilIsUtilityVm()) + // + // Parse and consume any transport= override from the option string. The + // option is always honored on WSL2: if the user requests a transport + // whose backend the host has not set up, the underlying mount syscall + // (or the virtiofs RPC) will fail with a clear error. On WSL1 there is + // only the in-kernel drvfs driver, so the chosen value is ignored, but + // the token must still be stripped so it does not leak into mount(2). + // + + std::string OptionsBuffer = Options ? Options : ""; + const auto Transport = ExtractTransportOption(OptionsBuffer); + const char* EffectiveOptions = OptionsBuffer.c_str(); + + if (Transport == DrvFsTransport::Invalid) { - return MountFilesystem(DRVFS_FS_TYPE, Source, Target, Options, ExitCode); + fprintf(stderr, "mount: invalid transport= value; valid options are: plan9, virtio9p, virtiofs\n"); + if (ExitCode) + { + *ExitCode = c_exitCodeInvalidUsage; + } + + return -1; } - else if (WSL_USE_VIRTIO_FS()) + + if (!UtilIsUtilityVm()) { - return MountVirtioFs(Source, Target, Options, Admin, Config, ExitCode); + if (Transport != DrvFsTransport::Default) + { + LOG_WARNING("transport= mount option is ignored on WSL1"); + } + + return MountFilesystem(DRVFS_FS_TYPE, Source, Target, EffectiveOptions, ExitCode); } - return MountPlan9(Source, Target, Options, Admin, Config, ExitCode); + switch (Transport) + { + case DrvFsTransport::Virtiofs: + return MountVirtioFs(Source, Target, EffectiveOptions, Admin, Config, ExitCode, false); + + case DrvFsTransport::Plan9: + case DrvFsTransport::Virtio9p: + return MountPlan9(Source, Target, EffectiveOptions, Admin, Config, ExitCode, Transport); + + case DrvFsTransport::Default: + default: + if (WSL_USE_VIRTIO_FS()) + { + return MountVirtioFs(Source, Target, EffectiveOptions, Admin, Config, ExitCode); + } + + return MountPlan9(Source, Target, EffectiveOptions, Admin, Config, ExitCode); + } } CATCH_RETURN_ERRNO() @@ -409,7 +565,7 @@ Return Value: return ExitCode; } -int MountPlan9Share(const char* Source, const char* Target, const char* Options, bool Admin, int* ExitCode) +int MountPlan9Share(const char* Source, const char* Target, const char* Options, bool Admin, DrvFsTransport Transport, int* ExitCode) /*++ @@ -427,6 +583,10 @@ Routine Description: Admin - Supplies a boolean specifying if the admin share should be used. + Transport - Supplies an optional transport override. When + DrvFsTransport::Default, the global feature flag (WSL_USE_VIRTIO_9P) + is consulted to pick between virtio9p and plan9-over-hvsocket. + ExitCode - Supplies an optional pointer that receives the exit code. Return Value: @@ -436,8 +596,23 @@ Return Value: --*/ { + bool UseVirtio9p; + switch (Transport) + { + case DrvFsTransport::Plan9: + UseVirtio9p = false; + break; + case DrvFsTransport::Virtio9p: + UseVirtio9p = true; + break; + case DrvFsTransport::Default: + default: + UseVirtio9p = WSL_USE_VIRTIO_9P(); + break; + } + std::string MountOptions; - if (WSL_USE_VIRTIO_9P()) + if (UseVirtio9p) { Source = Admin ? LX_INIT_DRVFS_ADMIN_VIRTIO_TAG : LX_INIT_DRVFS_VIRTIO_TAG; MountOptions = std::format("msize=262144,trans=virtio,{}", Options); @@ -459,7 +634,7 @@ Return Value: } } -int MountPlan9(const char* Source, const char* Target, const char* Options, std::optional Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode) +int MountPlan9(const char* Source, const char* Target, const char* Options, std::optional Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode, DrvFsTransport Transport) /*++ @@ -534,7 +709,7 @@ try // MountOptions += Plan9Options; - if (MountPlan9Share(Source, Target, MountOptions.c_str(), Elevated, ExitCode) < 0) + if (MountPlan9Share(Source, Target, MountOptions.c_str(), Elevated, Transport, ExitCode) < 0) { return -1; } @@ -543,7 +718,7 @@ try } CATCH_RETURN_ERRNO() -int MountVirtioFs(const char* Source, const char* Target, const char* Options, std::optional Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode) +int MountVirtioFs(const char* Source, const char* Target, const char* Options, std::optional Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode, bool AllowFallback) /*++ @@ -573,8 +748,6 @@ Return Value: try { - assert(WSL_USE_VIRTIO_FS()); - // // Check whether to use the elevated or non-elevated virtiofs server. // @@ -603,19 +776,26 @@ try AddShare.WriteString(AddShare->OptionsOffset, Plan9Options); // - // Connect to the wsl service to add the virtiofs share. If adding the share fails, fallback to mounting using Plan9. + // Connect to the wsl service to add the virtiofs share. // - wsl::shared::SocketChannel Channel{UtilConnectVsock(LX_INIT_UTILITY_VM_VIRTIOFS_PORT, true), "VirtoFs"}; - if (Channel.Socket() < 0) + wil::unique_fd channelFd{UtilConnectVsock(LX_INIT_UTILITY_VM_VIRTIOFS_PORT, true).release()}; + if (!channelFd) { return -1; } + wsl::shared::SocketChannel Channel{std::move(channelFd), "VirtioFs"}; + gsl::span ResponseSpan; const auto& Response = Channel.Transaction(AddShare.Span(), &ResponseSpan); if (Response.Result != 0) { + if (!AllowFallback) + { + return -1; + } + LOG_WARNING("Add virtiofs share for {} failed {}, falling back to Plan9", Source, Response.Result); return MountPlan9(Source, Target, Options, Admin, Config, ExitCode); } @@ -667,8 +847,6 @@ Return Value: try { - assert(WSL_USE_VIRTIO_FS()); - wsl::shared::MessageWriter RemountShare(LxInitMessageRemountVirtioFsDevice); RemountShare->Admin = Admin; RemountShare.WriteString(RemountShare->TagOffset, Tag); @@ -722,11 +900,6 @@ Return Value: try { - if (!WSL_USE_VIRTIO_FS()) - { - return {}; - } - // // Validate the tag is a GUID. // diff --git a/src/linux/init/drvfs.h b/src/linux/init/drvfs.h index 682e08d1f..fc09631a3 100644 --- a/src/linux/init/drvfs.h +++ b/src/linux/init/drvfs.h @@ -14,20 +14,61 @@ Module Name: #pragma once #include +#include #include "WslDistributionConfig.h" #define DRVFS_FS_TYPE "drvfs" #define MOUNT_DRVFS_NAME "mount.drvfs" +// +// DrvFs transport selector used by the per-mount transport override. +// +// Default - Use the global default selected by feature flags. +// Plan9 - Force plan9 over hvsocket. +// Virtio9p - Force plan9 over virtio (virtio-9p). +// Virtiofs - Force virtiofs. +// +enum class DrvFsTransport +{ + Default, + Invalid, + Plan9, + Virtio9p, + Virtiofs, +}; + +// +// Parses a transport= token out of a DrvFs option string. Any matching +// token is removed from Options regardless of position. The return value is +// the parsed transport, or DrvFsTransport::Default if no override was +// supplied. Invalid or empty values return DrvFsTransport::Invalid. If multiple +// transport= tokens are present, the last one wins (the others are dropped). +// +DrvFsTransport ExtractTransportOption(std::string& Options); + int MountDrvfs(const char* Source, const char* Target, const char* Options, std::optional Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode = nullptr); int MountDrvfsEntry(int Argc, char* Argv[]); -int MountPlan9Share(const char* Source, const char* Target, const char* Options, bool Admin, int* ExitCode = nullptr); +int MountPlan9Share(const char* Source, const char* Target, const char* Options, bool Admin, DrvFsTransport Transport = DrvFsTransport::Default, int* ExitCode = nullptr); -int MountPlan9(const char* Source, const char* Target, const char* Options, std::optional Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode); +int MountPlan9( + const char* Source, + const char* Target, + const char* Options, + std::optional Admin, + const wsl::linux::WslDistributionConfig& Config, + int* ExitCode, + DrvFsTransport Transport = DrvFsTransport::Default); -int MountVirtioFs(const char* Source, const char* Target, const char* Options, std::optional Admin, const wsl::linux::WslDistributionConfig& Config, int* ExitCode = nullptr); +int MountVirtioFs( + const char* Source, + const char* Target, + const char* Options, + std::optional Admin, + const wsl::linux::WslDistributionConfig& Config, + int* ExitCode = nullptr, + bool AllowFallback = true); int RemountVirtioFs(const char* Tag, const char* Target, const char* Options, bool Admin); diff --git a/src/windows/common/WslCoreConfig.cpp b/src/windows/common/WslCoreConfig.cpp index 3b9572183..9e4818fb2 100644 --- a/src/windows/common/WslCoreConfig.cpp +++ b/src/windows/common/WslCoreConfig.cpp @@ -124,7 +124,8 @@ void wsl::core::Config::ParseConfigFile(_In_opt_ LPCWSTR ConfigFilePath, _In_opt ConfigKey(ConfigSetting::Experimental::InitialAutoProxyTimeout, InitialAutoProxyTimeout), ConfigKey(ConfigSetting::Experimental::IgnoredPorts, std::move(parseIgnoredPorts)), ConfigKey(ConfigSetting::Experimental::HostAddressLoopback, EnableHostAddressLoopback), - ConfigKey(ConfigSetting::Experimental::SetVersionDebug, SetVersionDebug)}; + ConfigKey(ConfigSetting::Experimental::SetVersionDebug, SetVersionDebug), + ConfigKey(ConfigSetting::Experimental::DrvFsTransports, DrvFsTransports)}; wil::unique_file ConfigFile; if (ConfigFilePath != nullptr) @@ -448,12 +449,19 @@ void wsl::core::Config::Initialize(_In_opt_ HANDLE UserToken) VALIDATE_CONFIG_OPTION(EnableSafeMode, NetworkingMode, NetworkingMode::None); VALIDATE_CONFIG_OPTION(EnableSafeMode, EnableDnsTunneling, false); VALIDATE_CONFIG_OPTION(EnableSafeMode, EnableAutoProxy, false); + VALIDATE_CONFIG_OPTION(EnableSafeMode, DrvFsTransports, false); } if (!EnableVirtio) { VALIDATE_CONFIG_OPTION(!EnableVirtio, EnableVirtio9p, false); VALIDATE_CONFIG_OPTION(!EnableVirtio, EnableVirtioFs, false); + VALIDATE_CONFIG_OPTION(!EnableVirtio, DrvFsTransports, false); + } + + if (!EnableHostFileSystemAccess) + { + VALIDATE_CONFIG_OPTION(!EnableHostFileSystemAccess, DrvFsTransports, false); } if (EnableVirtio9p) diff --git a/src/windows/common/WslCoreConfig.h b/src/windows/common/WslCoreConfig.h index 486c78451..944aa4026 100644 --- a/src/windows/common/WslCoreConfig.h +++ b/src/windows/common/WslCoreConfig.h @@ -27,7 +27,7 @@ Module Name: T_VALUE(c, EnableHostAddressLoopback), T_VALUE(c, EnableHostFileSystemAccess), T_VALUE(c, EnableIpv6), \ T_VALUE(c, EnableLocalhostRelay), T_VALUE(c, EnableNestedVirtualization), T_VALUE(c, EnableSafeMode), \ T_VALUE(c, EnableSparseVhd), T_VALUE(c, EnableVirtio), T_VALUE(c, EnableVirtio9p), T_VALUE(c, EnableVirtioFs), \ - T_ENUM(c, FirewallConfigPresence), T_VALUE(c, KernelBootTimeout), T_SET(c, KernelCommandLine), \ + T_VALUE(c, DrvFsTransports), T_ENUM(c, FirewallConfigPresence), T_VALUE(c, KernelBootTimeout), T_SET(c, KernelCommandLine), \ T_VALUE(c, KernelDebugPort), T_SET(c, KernelModulesPath), T_STRING(c, KernelModulesList), T_SET(c, KernelPath), \ T_VALUE(c, LoadDefaultKernelModules), T_PRESENT(c, LoadKernelModulesPresence), T_VALUE(c, MaximumMemorySizeBytes), \ T_VALUE(c, MaximumProcessorCount), T_ENUM(c, MemoryReclaim), T_VALUE(c, MemorySizeBytes), T_VALUE(c, MountDeviceTimeout), \ @@ -289,6 +289,7 @@ namespace ConfigSetting { static constexpr auto IgnoredPorts = "experimental.ignoredPorts"; static constexpr auto HostAddressLoopback = "experimental.hostAddressLoopback"; static constexpr auto SetVersionDebug = "experimental.setVersionDebug"; + static constexpr auto DrvFsTransports = "experimental.drvFsTransports"; } // namespace Experimental } // namespace ConfigSetting @@ -304,6 +305,22 @@ struct Config void SaveNetworkingSettings(_In_opt_ HANDLE UserToken) const; static unsigned long WriteConfigFile(_In_ LPCWSTR ConfigFilePath, _In_ ConfigKey KeyToWrite, _In_ bool RemoveKey = false); + // True when the host should set up the virtio9p plan9 server for DrvFs. + // The experimental DrvFsTransports option forces it on so that all three + // DrvFs transports can be tested concurrently in a single VM. + bool IsVirtio9pTransportAvailable() const noexcept + { + return EnableVirtio9p || DrvFsTransports; + } + + // True when the host should be able to serve virtiofs shares for DrvFs. + // The experimental DrvFsTransports option forces it on so that all three + // DrvFs transports can be tested concurrently in a single VM. + bool IsVirtioFsTransportAvailable() const noexcept + { + return EnableVirtioFs || DrvFsTransports; + } + std::filesystem::path KernelPath; std::wstring KernelCommandLine; std::wstring KernelModulesList; @@ -324,6 +341,7 @@ struct Config bool EnableVirtio9p = false; bool EnableVirtio = !shared::Arm64 || windows::common::helpers::IsWindows11OrAbove(); bool EnableVirtioFs = false; + bool DrvFsTransports = false; int KernelDebugPort = 0; bool EnableGpuSupport = true; bool EnableGuiApps = true; diff --git a/src/windows/service/exe/WslCoreVm.cpp b/src/windows/service/exe/WslCoreVm.cpp index 3c08097f5..314967520 100644 --- a/src/windows/service/exe/WslCoreVm.cpp +++ b/src/windows/service/exe/WslCoreVm.cpp @@ -868,50 +868,67 @@ void WslCoreVm::AddDrvFsShare(_In_ bool Admin, _In_ HANDLE UserToken) } _Requires_lock_held_(m_guestDeviceLock) -void WslCoreVm::AddPlan9Share( - _In_ PCWSTR AccessName, _In_ PCWSTR Path, [[maybe_unused]] _In_ UINT32 Port, _In_ hcs::Plan9ShareFlags Flags, _In_ HANDLE UserToken, _In_opt_ PCWSTR VirtIoTag) +void WslCoreVm::AddPlan9Share(_In_ PCWSTR AccessName, _In_ PCWSTR Path, _In_ UINT32 Port, _In_ hcs::Plan9ShareFlags Flags, _In_ HANDLE UserToken, _In_opt_ PCWSTR VirtIoTag) { + // Decide which plan9 transports to register the share with. In the default + // configuration this is exactly one (virtio9p OR hvsocket), but the + // experimental DrvFsTransports option forces both so that all three DrvFs + // transports (plan9-over-hvsocket, virtio9p, virtiofs) are available + // concurrently for testing. + const bool wantVirtio9p = m_vmConfig.IsVirtio9pTransportAvailable(); + const bool wantHvsocket = !m_vmConfig.EnableVirtio9p || m_vmConfig.DrvFsTransports; + + // This is called from AddDrvFsShare, which is called from InitializeDrvFs, + // so m_guestDeviceLock is already held. + bool addNewDevice = false; - wil::com_ptr server; + wil::com_ptr virtio9pServer; + if (wantVirtio9p) { + WI_ASSERT(VirtIoTag != nullptr); + auto revert = wil::impersonate_token(UserToken); - // This is called from AddDrvFsShare, which is called from InitializeDrvFs, so m_guestDeviceLock is - // already held. + virtio9pServer = m_guestDeviceManager->GetRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), VirtIoTag); + if (!virtio9pServer) + { + virtio9pServer = wsl::windows::common::wslutil::CreateComServerAsUser(UserToken); + m_guestDeviceManager->AddRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), VirtIoTag, virtio9pServer); + + // Start with one device to handle the first mount request. After + // each mount, the Plan9 file-system will request additional + // devices via the IPlan9FileSystemHost::NotifyAllDevicesInUse + // callback. + addNewDevice = true; + } - if (m_vmConfig.EnableVirtio9p) + HRESULT result = virtio9pServer->AddSharePath(AccessName, Path, static_cast(Flags)); + if (result == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)) { - server = m_guestDeviceManager->GetRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), VirtIoTag); + result = S_OK; } - else + + THROW_IF_FAILED(result); + } + + if (wantHvsocket) + { + auto revert = wil::impersonate_token(UserToken); + + wil::com_ptr server; + const auto existingServer = m_plan9Servers.find(Port); + if (existingServer != m_plan9Servers.end()) { - const auto existingServer = m_plan9Servers.find(Port); - if (existingServer != m_plan9Servers.end()) - { - server = existingServer->second; - } + server = existingServer->second; } if (!server) { server = wsl::windows::common::wslutil::CreateComServerAsUser(UserToken); - if (m_vmConfig.EnableVirtio9p) - { - m_guestDeviceManager->AddRemoteFileSystem(__uuidof(p9fs::Plan9FileSystem), VirtIoTag, server); - - // Start with one device to handle the first mount request. After - // each mount, the Plan9 file-system will request additional - // devices via the IPlan9FileSystemHost::NotifyAllDevicesInUse - // callback. - addNewDevice = true; - } - else - { - THROW_IF_FAILED(server->Init(&m_runtimeId, Port)); - THROW_IF_FAILED(server->Resume()); - m_plan9Servers.insert(std::make_pair(Port, server)); - } + THROW_IF_FAILED(server->Init(&m_runtimeId, Port)); + THROW_IF_FAILED(server->Resume()); + m_plan9Servers.insert(std::make_pair(Port, server)); } HRESULT result = server->AddSharePath(AccessName, Path, static_cast(Flags)); @@ -926,7 +943,7 @@ void WslCoreVm::AddPlan9Share( if (addNewDevice) { // This requires more privileges than the user may have, so impersonation is disabled. - (void)m_guestDeviceManager->AddNewDevice(VIRTIO_PLAN9_DEVICE_ID, server, VirtIoTag); + (void)m_guestDeviceManager->AddNewDevice(VIRTIO_PLAN9_DEVICE_ID, virtio9pServer, VirtIoTag); } } @@ -1533,7 +1550,8 @@ std::wstring WslCoreVm::GenerateConfigJson() helpers::AppendCommonKernelCommandLine(kernelCmdLine, m_pageReportingOrder); // 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) + if (m_vmConfig.IsVirtio9pTransportAvailable() || m_vmConfig.IsVirtioFsTransportAvailable() || + m_vmConfig.NetworkingMode == NetworkingMode::VirtioProxy) { kernelCmdLine += L" swiotlb=force"; } @@ -2128,7 +2146,7 @@ void WslCoreVm::WaitForPmemDeviceInVm(_In_ ULONG PmemId) _Requires_lock_held_(m_guestDeviceLock) std::pair WslCoreVm::AddVirtioFsShare(_In_ bool Admin, _In_ PCWSTR Path, _In_ PCWSTR Options, _In_opt_ HANDLE UserToken) { - WI_ASSERT(m_vmConfig.EnableVirtioFs); + WI_ASSERT(m_vmConfig.IsVirtioFsTransportAvailable()); if (!ARGUMENT_PRESENT(UserToken)) { @@ -2398,7 +2416,7 @@ void WslCoreVm::RegisterCallbacks(_In_ const std::function& DistroE } } - if (m_vmConfig.EnableHostFileSystemAccess && m_vmConfig.EnableVirtioFs) + if (m_vmConfig.EnableHostFileSystemAccess && m_vmConfig.IsVirtioFsTransportAvailable()) { // Create a thread listening for handling virtiofs requests. auto listenSocket = wsl::windows::common::hvsocket::Listen(m_runtimeId, LX_INIT_UTILITY_VM_VIRTIOFS_PORT); diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index d18622dd6..14770fd8b 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -1601,6 +1601,13 @@ std::wstring LxssGenerateTestConfig(TestConfigDefaults Default) newConfig += L"[wsl2]\n"; } + if (Default.drvFsTransports.has_value()) + { + newConfig += L"\n[experimental]\n"; + newConfig += boolOptionToString(L"drvFsTransports", Default.drvFsTransports, false); + newConfig += L"[wsl2]\n"; + } + // TODO: Remove once SetVersion() truncated archive error is root caused. newConfig += L"\n[experimental]\nSetVersionDebug=true\n[wsl2]\n"; diff --git a/test/windows/Common.h b/test/windows/Common.h index ad2706ebf..64426cef4 100644 --- a/test/windows/Common.h +++ b/test/windows/Common.h @@ -521,6 +521,7 @@ struct TestConfigDefaults std::optional safeMode; std::optional guiApplications; std::optional drvFsMode; + std::optional drvFsTransports; std::optional networkingMode; const std::optional vmSwitch; const std::optional macAddress; diff --git a/test/windows/DrvFsTests.cpp b/test/windows/DrvFsTests.cpp index 9025ba6a3..c5c87764e 100644 --- a/test/windows/DrvFsTests.cpp +++ b/test/windows/DrvFsTests.cpp @@ -1313,4 +1313,208 @@ WSL2_DRVFS_TEST_CLASS(VirtioFs); // TODO: Enable again once the issue is resolved // WSL2_DRVFS_TEST_CLASS(Virtio9p); +// +// Tests for the per-mount transport= option and the +// experimental.drvFsTransports setting, which together let a single VM +// expose all three DrvFs transports concurrently. +// + +class DrvFsTransportOverride +{ + WSL_TEST_CLASS(DrvFsTransportOverride) + + std::unique_ptr m_config; + + TEST_CLASS_SETUP(TestClassSetup) + { + if (!LxsstuVmMode()) + { + LogSkipped("This test class is only applicable to WSL2"); + } + else + { + VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE); + } + + return true; + } + + TEST_CLASS_CLEANUP(TestClassCleanup) + { + m_config.reset(); + if (LxsstuVmMode()) + { + LxsstuUninitialize(FALSE); + } + return true; + } + + static void UnmountAndRemove(const wchar_t* MountPoint) + { + LxsstuLaunchWsl(std::format(L"-u root umount '{}'", MountPoint).c_str()); + LxsstuLaunchWsl(std::format(L"-u root rmdir '{}'", MountPoint).c_str()); + } + + struct MountPointGuard + { + const wchar_t* path; + ~MountPointGuard() + { + UnmountAndRemove(path); + } + }; + + static MountPointGuard CreateMountPoint(const wchar_t* MountPoint) + { + UnmountAndRemove(MountPoint); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(std::format(L"-u root mkdir -p '{}'", MountPoint).c_str()), 0); + return {MountPoint}; + } + + static std::wstring GrepMountLine(const wchar_t* MountPoint) + { + auto [out, err] = LxsstuLaunchWslAndCaptureOutput(std::format(L"mount | grep -F ' {} type ' | head -n1", MountPoint).c_str()); + return out; + } + + // + // With experimental.drvFsTransports=true the host stands up all three + // transport backends, so each of the three transport values produces a + // working mount with the expected filesystem type. + // + WSL2_TEST_METHOD(MountAllThreeTransports) + { + m_config.reset(new WslConfigChange(LxssGenerateTestConfig({.drvFsTransports = true}))); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), 0); + + struct Case + { + const wchar_t* mountPoint; + const wchar_t* transport; + const wchar_t* expectedType; + }; + + const Case cases[] = { + {L"/tmp/drvfs_plan9", L"transport=plan9", L"9p"}, + {L"/tmp/drvfs_virtio9p", L"transport=virtio9p", L"9p"}, + {L"/tmp/drvfs_virtiofs", L"transport=virtiofs", L"virtiofs"}, + }; + + for (const auto& c : cases) + { + auto guard = CreateMountPoint(c.mountPoint); + + const auto mountCmd = std::format(L"-u root mount -t drvfs C: '{}' -o {}", c.mountPoint, c.transport); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(mountCmd.c_str()), 0, c.transport); + + const auto grepOut = GrepMountLine(c.mountPoint); + LogInfo("transport=%ls -> %ls", c.transport, grepOut.c_str()); + + const std::wstring expectedToken = std::wstring(L" type ") + c.expectedType + L" "; + VERIFY_IS_TRUE( + grepOut.find(expectedToken) != std::wstring::npos, + std::format(L"Expected '{}' in mount line for transport {}", expectedToken, c.transport).c_str()); + + // transport= must be consumed by mount.drvfs and not leak into mount(2). + VERIFY_IS_TRUE( + grepOut.find(L"transport=") == std::wstring::npos, + L"transport= option must be stripped before passing to mount(2)"); + + // Basic IO sanity check. + auto [lsOut, lsErr] = LxsstuLaunchWslAndCaptureOutput(std::format(L"ls '{}/Windows'", c.mountPoint)); + VERIFY_IS_TRUE(!lsOut.empty(), c.transport); + } + } + + // + // transport= at the start, middle, and end of the options string is + // correctly extracted, and other options (like uid=) are preserved. + // + WSL2_TEST_METHOD(TransportOptionPositions) + { + m_config.reset(new WslConfigChange(LxssGenerateTestConfig({.drvFsTransports = true}))); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), 0); + + struct Case + { + const wchar_t* mountPoint; + const wchar_t* options; + }; + + const Case cases[] = { + {L"/tmp/drvfs_pos_first", L"transport=plan9,uid=1000,gid=1000"}, + {L"/tmp/drvfs_pos_mid", L"uid=1000,transport=plan9,gid=1000"}, + {L"/tmp/drvfs_pos_last", L"uid=1000,gid=1000,transport=plan9"}, + }; + + for (const auto& c : cases) + { + auto guard = CreateMountPoint(c.mountPoint); + + const auto mountCmd = std::format(L"-u root mount -t drvfs C: '{}' -o {}", c.mountPoint, c.options); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(mountCmd.c_str()), 0, c.options); + + const auto grepOut = GrepMountLine(c.mountPoint); + VERIFY_IS_TRUE(grepOut.find(L" type 9p ") != std::wstring::npos, c.options); + VERIFY_IS_TRUE(grepOut.find(L"transport=") == std::wstring::npos, L"transport= must be consumed"); + VERIFY_IS_TRUE(grepOut.find(L"uid=1000") != std::wstring::npos, L"uid= must be preserved"); + } + } + + // + // Unknown and empty transport values fail the mount with an error message + // to stderr rather than silently falling back to the default. + // + WSL2_TEST_METHOD(TransportInvalidReturnsError) + { + m_config.reset(new WslConfigChange(LxssGenerateTestConfig({.drvFsTransports = true}))); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), 0); + + for (const wchar_t* opt : {L"transport=bogus", L"transport="}) + { + const wchar_t* mountPoint = L"/tmp/drvfs_invalid"; + auto guard = CreateMountPoint(mountPoint); + + // Mount must fail with a non-zero exit code. + VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(std::format(L"-u root mount -t drvfs C: '{}' -o {}", mountPoint, opt).c_str()), 0, opt); + + const auto grepOut = GrepMountLine(mountPoint); + VERIFY_IS_TRUE(grepOut.empty(), std::format(L"mount with '{}' must fail (not silently succeed)", opt).c_str()); + } + } + + // + // When the experimental flag is OFF (default), requesting a transport + // whose backend is not running in the VM must fail at mount time. In + // Plan9 mode neither virtio9p nor virtiofs is up, so both fail. + // + WSL2_TEST_METHOD(TransportFailsWhenBackendUnavailable) + { + m_config.reset(new WslConfigChange(LxssGenerateTestConfig({.drvFsMode = DrvFsMode::Plan9}))); + VERIFY_ARE_EQUAL(LxsstuLaunchWsl(LXSST_TESTS_INSTALL_COMMAND_LINE), 0); + + const wchar_t* mountPoint = L"/tmp/drvfs_unavail"; + + for (const wchar_t* transport : {L"transport=virtio9p", L"transport=virtiofs"}) + { + auto guard = CreateMountPoint(mountPoint); + + // Mount should fail because the backend isn't running. + VERIFY_ARE_NOT_EQUAL(LxsstuLaunchWsl(std::format(L"-u root mount -t drvfs C: '{}' -o {}", mountPoint, transport).c_str()), 0, transport); + + const auto grepOut = GrepMountLine(mountPoint); + VERIFY_IS_TRUE( + grepOut.empty(), + std::format(L"Mount with {} should fail when its backend is not running (got: {})", transport, grepOut).c_str()); + } + + // Sanity: the default backend still works. + auto guard = CreateMountPoint(mountPoint); + VERIFY_ARE_EQUAL( + LxsstuLaunchWsl(std::format(L"-u root mount -t drvfs C: '{}' -o transport=plan9", mountPoint).c_str()), + 0, + L"transport=plan9 should succeed in plan9 default mode"); + } +}; + } // namespace DrvFsTests \ No newline at end of file