@samitouri / QOSAMI-WSL / commits / aef5b0ff

Fix VHD volume recovery (#40750)

Kevin Vega committed Jun 12, 2026 at 14:20 UTC aef5b0ff335db49323331d94f12b1449e862c1f3
13 files changed +237 -36
localization/strings/en-US/Resources.resw
+8
@@ -3236,6 +3236,14 @@ On first run, creates the file with all settings commented out at their defaults
3236 <value>Failed to recover volume '{}'</value>
3237 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3238 </data>
3239 + <data name="MessageWslcVolumeNotAvailable" xml:space="preserve">
3240 + <value>Cannot start container because the following volumes are not available: {}</value>
3241 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3242 + </data>
3243 + <data name="MessageWslcVolumeNotAvailableReason" xml:space="preserve">
3244 + <value>Volume '{}' is not available: {}</value>
3245 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3246 + </data>
3247 <data name="MessageWslcSwapInitFailed" xml:space="preserve">
3248 <value>Failed to initialize swap</value>
3249 </data>
src/windows/WslcSDK/wslcsdk.h
+1
@@ -40,6 +40,7 @@ EXTERN_C_START
40 #define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */
41 #define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */
42 #define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */
43 +#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */
44
45 // Session values
46 #define WSLC_SESSION_OPTIONS_SIZE 88
src/windows/common/wslutil.cpp
+1
@@ -154,6 +154,7 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
154 X(WSLC_E_IMAGE_NOT_FOUND),
155 X(WSLC_E_CONTAINER_NOT_FOUND),
156 X(WSLC_E_VOLUME_NOT_FOUND),
157 + X(WSLC_E_VOLUME_NOT_AVAILABLE),
158 X(WSLC_E_CONTAINER_NOT_RUNNING),
159 X(WSLC_E_CONTAINER_IS_RUNNING),
160 X(WSLC_E_SESSION_RESERVED),
src/windows/service/inc/wslc.idl
+1
@@ -948,3 +948,4 @@ cpp_quote("#define WSLC_E_WU_SEARCH_FAILED MAKE_HRESULT(SEVERITY_ERROR, FACILITY
948 cpp_quote("#define WSLC_E_SDK_UPDATE_NEEDED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 11) /* 0x8004060B */")
949 cpp_quote("#define WSLC_E_CONTAINER_DISABLED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 12) /* 0x8004060C */")
950 cpp_quote("#define WSLC_E_REGISTRY_BLOCKED_BY_POLICY MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 13) /* 0x8004060D */")
951 +cpp_quote("#define WSLC_E_VOLUME_NOT_AVAILABLE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 14) /* 0x8004060E */")
src/windows/wslcsession/IWSLCVolume.h
+9
@@ -20,6 +20,7 @@ Abstract:
20 #include "wslc.h"
21 #include <map>
22 #include <string>
23 +#include <utility>
24
25 namespace wsl::windows::service::wslc {
26
@@ -39,6 +40,14 @@ public:
40 // The user-specified labels on this volume (excludes the WSLC metadata label).
41 virtual const std::map<std::string, std::string>& Labels() const noexcept = 0;
42
43 + // The status of the volume as {Code, Message}: S_OK with an empty message when the volume
44 + // opened successfully and is usable, otherwise a failure HRESULT and a human-readable reason
45 + // (e.g. the backing VHD is missing).
46 + virtual std::pair<HRESULT, std::string> Status() const
47 + {
48 + return {S_OK, {}};
49 + }
50 +
51 // Remove the volume from docker and release any host-side resources
52 // (e.g. detach/delete the VHD for VHD volumes). Throws on failure.
53 virtual void Delete() = 0;
src/windows/wslcsession/WSLCContainer.cpp
+38 -9
@@ -524,6 +524,8 @@ WSLCContainerImpl::WSLCContainerImpl(
524 std::string&& Image,
525 std::string NetworkMode,
526 std::vector<WSLCVolumeMount>&& volumes,
527 + std::vector<std::string>&& namedVolumes,
528 + WSLCVolumes& Volumes,
529 std::vector<ContainerPortMapping>&& ports,
530 std::map<std::string, std::string>&& labels,
531 std::function<void(const WSLCContainerImpl*)>&& onDeleted,
@@ -542,6 +544,8 @@ WSLCContainerImpl::WSLCContainerImpl(
544 m_networkMode(std::move(NetworkMode)),
545 m_id(std::move(Id)),
546 m_mountedVolumes(std::move(volumes)),
547 + m_namedVolumes(std::move(namedVolumes)),
548 + m_volumes(Volumes),
549 m_mappedPorts(std::move(ports)),
550 m_labels(std::move(labels)),
551 m_comWrapper(wil::MakeOrThrow<WSLCContainer>(this, wslcSession, std::move(onDeleted))),
@@ -790,6 +794,23 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
794 m_initProcessControl = nullptr;
795 });
796
797 + // Refuse to start if any referenced named volume is in a failed state.
798 + std::vector<std::string> unavailableVolumes;
799 + for (const auto& volumeName : m_namedVolumes)
800 + {
801 + const auto [code, message] = m_volumes.GetVolumeStatus(volumeName);
802 + if (FAILED(code))
803 + {
804 + EMIT_USER_WARNING(Localization::MessageWslcVolumeNotAvailableReason(volumeName, message));
805 + unavailableVolumes.push_back(volumeName);
806 + }
807 + }
808 +
809 + THROW_HR_WITH_USER_ERROR_IF(
810 + WSLC_E_VOLUME_NOT_AVAILABLE,
811 + Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')),
812 + !unavailableVolumes.empty());
813 +
814 auto volumeCleanup = MountVolumes(m_mountedVolumes, m_virtualMachine);
815
816 auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
@@ -1363,6 +1384,7 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1384 WSLCVirtualMachine& virtualMachine,
1385 IWSLCPluginNotifier* pluginNotifier,
1386 const std::unordered_map<std::string, NetworkEntry>& sessionNetworks,
1387 + WSLCVolumes& volumesManager,
1388 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
1389 DockerEventTracker& EventTracker,
1390 DockerHTTPClient& DockerClient,
@@ -1731,6 +1753,15 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1753 // from this function.
1754 EventTracker.WaitForObjectCreated(result.Id);
1755
1756 + // Collect the names of referenced docker named volumes so Start() can verify
1757 + // they are available before running the container.
1758 + std::vector<std::string> namedVolumes;
1759 + namedVolumes.reserve(containerOptions.NamedVolumesCount);
1760 + for (ULONG i = 0; i < containerOptions.NamedVolumesCount; i++)
1761 + {
1762 + namedVolumes.emplace_back(containerOptions.NamedVolumes[i].Name);
1763 + }
1764 +
1765 auto container = std::make_unique<WSLCContainerImpl>(
1766 wslcSession,
1767 virtualMachine,
@@ -1740,6 +1771,8 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1771 std::string(containerOptions.Image),
1772 std::move(networkMode),
1773 std::move(volumes),
1774 + std::move(namedVolumes),
1775 + volumesManager,
1776 std::move(mappedPorts),
1777 std::move(labels),
1778 std::move(OnDeleted),
@@ -1769,19 +1802,13 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1802 // Extract container name from Docker's names list.
1803 std::string name = ExtractContainerName(dockerContainer.Names, dockerContainer.Id);
1804
1772 - // Validate that all named volumes mounted by the container were successfully recovered
1773 - // by the volumes manager. If any are missing (e.g. backing VHD removed while the service was
1774 - // down), refuse to open the container so it cannot enter a broken state.
1805 + // Collect the names of referenced docker named volumes.
1806 + std::vector<std::string> namedVolumes;
1807 for (const auto& mount : dockerContainer.Mounts)
1808 {
1809 if (mount.Type == "volume" && !mount.Name.empty())
1810 {
1779 - THROW_HR_IF_MSG(
1780 - E_UNEXPECTED,
1781 - !volumes.ContainsVolume(mount.Name),
1782 - "Cannot open container %hs: referenced volume '%hs' is not available",
1783 - dockerContainer.Id.c_str(),
1784 - mount.Name.c_str());
1811 + namedVolumes.push_back(mount.Name);
1812 }
1813 }
1814
@@ -1836,6 +1863,8 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Open(
1863 std::string(dockerContainer.Image),
1864 std::move(networkMode),
1865 std::move(metadata.Volumes),
1866 + std::move(namedVolumes),
1867 + volumes,
1868 std::move(ports),
1869 std::move(labels),
1870 std::move(OnDeleted),
src/windows/wslcsession/WSLCContainer.h
+7
@@ -78,6 +78,8 @@ public:
78 std::string&& Image,
79 std::string NetworkMode,
80 std::vector<WSLCVolumeMount>&& volumes,
81 + std::vector<std::string>&& namedVolumes,
82 + WSLCVolumes& Volumes,
83 std::vector<ContainerPortMapping>&& ports,
84 std::map<std::string, std::string>&& labels,
85 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
@@ -135,6 +137,7 @@ public:
137 WSLCVirtualMachine& virtualMachine,
138 IWSLCPluginNotifier* pluginNotifier,
139 const std::unordered_map<std::string, NetworkEntry>& SessionNetworks,
140 + WSLCVolumes& Volumes,
141 std::function<void(const WSLCContainerImpl*)>&& OnDeleted,
142 DockerEventTracker& EventTracker,
143 DockerHTTPClient& DockerClient,
@@ -208,6 +211,10 @@ private:
211 WSLCVirtualMachine& m_virtualMachine;
212 std::vector<ContainerPortMapping> m_mappedPorts;
213 std::vector<WSLCVolumeMount> m_mountedVolumes;
214 +
215 + std::vector<std::string> m_namedVolumes;
216 + WSLCVolumes& m_volumes;
217 +
218 std::map<std::string, std::string> m_labels;
219 Microsoft::WRL::ComPtr<WSLCContainer> m_comWrapper;
220 DockerEventTracker& m_eventTracker;
src/windows/wslcsession/WSLCSession.cpp
+3 -1
@@ -1758,6 +1758,7 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
1758 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine);
1759 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_eventTracker);
1760 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient);
1761 + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_volumes);
1762
1763 // Validate that name & images are valid.
1764 if (containerOptions->Name != nullptr && containerOptions->Name[0] != '\0')
@@ -1807,6 +1808,7 @@ void WSLCSession::CreateContainerImpl(const WSLCContainerOptions* containerOptio
1808 m_virtualMachine.value(),
1809 m_pluginNotifier.get(),
1810 m_networks,
1811 + m_volumes.value(),
1812 std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
1813 m_eventTracker.value(),
1814 m_dockerClient.value(),
@@ -2979,7 +2981,7 @@ void WSLCSession::RecoverExistingContainers()
2981 *this,
2982 m_virtualMachine.value(),
2983 m_pluginNotifier.get(),
2982 - *m_volumes,
2984 + m_volumes.value(),
2985 std::bind(&WSLCSession::OnContainerDeleted, this, std::placeholders::_1),
2986 m_eventTracker.value(),
2987 m_dockerClient.value(),
src/windows/wslcsession/WSLCVhdVolume.cpp
+61 -14
@@ -92,19 +92,25 @@ WSLCVhdVolumeImpl::WSLCVhdVolumeImpl(
92 ULONGLONG SizeBytes,
93 ULONG Lun,
94 std::string&& VirtualMachinePath,
95 + std::string&& CreatedAt,
96 std::map<std::string, std::string>&& DriverOpts,
97 std::map<std::string, std::string>&& Labels,
98 WSLCVirtualMachine& VirtualMachine,
98 - DockerHTTPClient& DockerClient) :
99 + DockerHTTPClient& DockerClient,
100 + bool Attached,
101 + std::pair<HRESULT, std::string> Status) :
102 m_name(std::move(Name)),
103 m_hostPath(std::move(HostPath)),
104 m_virtualMachinePath(std::move(VirtualMachinePath)),
105 + m_createdAt(std::move(CreatedAt)),
106 m_driverOpts(std::move(DriverOpts)),
107 m_labels(std::move(Labels)),
108 m_sizeBytes(SizeBytes),
109 m_lun(Lun),
110 m_virtualMachine(VirtualMachine),
107 - m_dockerClient(DockerClient)
111 + m_dockerClient(DockerClient),
112 + m_attached(Attached),
113 + m_status(std::move(Status))
114 {
115 }
116
@@ -173,8 +179,16 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create(
179 auto createdVolume = DockerClient.CreateVolume(request);
180
181 auto volume = std::make_unique<WSLCVhdVolumeImpl>(
176 - std::move(name), std::move(hostPath), opts.SizeBytes, lun, std::move(virtualMachinePath), std::move(DriverOpts), std::move(Labels), VirtualMachine, DockerClient);
177 - volume->m_createdAt = createdVolume.CreatedAt;
182 + std::move(name),
183 + std::move(hostPath),
184 + opts.SizeBytes,
185 + lun,
186 + std::move(virtualMachinePath),
187 + std::move(createdVolume.CreatedAt),
188 + std::move(DriverOpts),
189 + std::move(Labels),
190 + VirtualMachine,
191 + DockerClient);
192
193 mountCleanup.release();
194 attachCleanup.release();
@@ -220,20 +234,47 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Open(
234 }
235 }
236
223 - auto [lun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false);
224 - auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(lun); });
237 + ULONG lun = 0;
238 + bool attached = false;
239 + std::pair<HRESULT, std::string> status{S_OK, {}};
240
226 - VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "", 0);
227 - auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
241 + try
242 + {
243 + auto [attachedLun, device] = VirtualMachine.AttachDisk(hostPath.c_str(), false);
244 + auto attachCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.DetachDisk(attachedLun); });
245
229 - auto volume = std::make_unique<WSLCVhdVolumeImpl>(
230 - std::string{Volume.Name}, std::move(hostPath), opts.SizeBytes, lun, std::move(virtualMachinePath), std::move(driverOpts), std::move(userLabels), VirtualMachine, DockerClient);
231 - volume->m_createdAt = Volume.CreatedAt;
246 + VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "", 0);
247 + auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
248
233 - mountCleanup.release();
234 - attachCleanup.release();
249 + lun = attachedLun;
250 + attached = true;
251
236 - return volume;
252 + mountCleanup.release();
253 + attachCleanup.release();
254 + }
255 + catch (...)
256 + {
257 + // The backing VHD could not be attached or mounted. Track the volume in an errored state so the user can still inspect
258 + // and delete it; containers that reference it should refuse to start. The reason is surfaced via Inspect(), not the warning.
259 + const auto hr = wil::ResultFromCaughtException();
260 + const auto message = wslutil::GetErrorString(hr);
261 + EMIT_USER_WARNING(Localization::MessageWslcFailedToRecoverVolume(Volume.Name));
262 + status = {hr, wsl::shared::string::WideToMultiByte(message)};
263 + }
264 +
265 + return std::make_unique<WSLCVhdVolumeImpl>(
266 + std::string{Volume.Name},
267 + std::move(hostPath),
268 + opts.SizeBytes,
269 + lun,
270 + std::move(virtualMachinePath),
271 + std::string{Volume.CreatedAt},
272 + std::move(driverOpts),
273 + std::move(userLabels),
274 + VirtualMachine,
275 + DockerClient,
276 + attached,
277 + std::move(status));
278 }
279
280 void WSLCVhdVolumeImpl::Delete()
@@ -266,6 +307,12 @@ std::string WSLCVhdVolumeImpl::Inspect() const
307 {"SizeBytes", std::to_string(m_sizeBytes)},
308 };
309
310 + // Surface the recovery failure so callers can see why the volume is unusable.
311 + if (FAILED(m_status.first))
312 + {
313 + inspect.Status->emplace("Error", m_status.second);
314 + }
315 +
316 return wsl::shared::ToJson(inspect);
317 }
318
src/windows/wslcsession/WSLCVhdVolume.h
+12 -2
@@ -42,10 +42,13 @@ public:
42 ULONGLONG SizeBytes,
43 ULONG Lun,
44 std::string&& VirtualMachinePath,
45 + std::string&& CreatedAt,
46 std::map<std::string, std::string>&& DriverOpts,
47 std::map<std::string, std::string>&& Labels,
48 WSLCVirtualMachine& VirtualMachine,
48 - DockerHTTPClient& DockerClient);
49 + DockerHTTPClient& DockerClient,
50 + bool Attached = true,
51 + std::pair<HRESULT, std::string> Status = {S_OK, {}});
52
53 ~WSLCVhdVolumeImpl();
54
@@ -74,6 +77,11 @@ public:
77 return m_labels;
78 }
79
80 + std::pair<HRESULT, std::string> Status() const override
81 + {
82 + return m_status;
83 + }
84 +
85 void Delete() override;
86 std::string Inspect() const override;
87 WSLCVolumeInformation GetVolumeInformation() const override;
@@ -97,7 +105,9 @@ private:
105 ULONG m_lun{};
106 WSLCVirtualMachine& m_virtualMachine;
107 DockerHTTPClient& m_dockerClient;
100 - bool m_attached{true};
108 + bool m_attached;
109 +
110 + std::pair<HRESULT, std::string> m_status;
111 };
112
113 } // namespace wsl::windows::service::wslc
src/windows/wslcsession/WSLCVolumes.cpp
+8 -5
@@ -44,8 +44,7 @@ WSLCVolumes::WSLCVolumes(
44 catch (...)
45 {
46 LOG_CAUGHT_EXCEPTION_MSG("Failed to recover volume: %hs", volume.Name.c_str());
47 - EMIT_USER_WARNING(
48 - wsl::shared::Localization::MessageWslcFailedToRecoverVolume(wsl::shared::string::MultiByteToWide(volume.Name)));
47 + EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcFailedToRecoverVolume(volume.Name));
48 }
49 }
50 }
@@ -203,10 +202,14 @@ std::string WSLCVolumes::InspectVolume(const std::string& Name) const
202 return it->second->Inspect();
203 }
204
206 -bool WSLCVolumes::ContainsVolume(const std::string& Name) const
205 +std::pair<HRESULT, std::string> WSLCVolumes::GetVolumeStatus(const std::string& Name) const
206 {
207 auto lock = m_lock.lock_shared();
209 - return m_volumes.contains(Name);
208 +
209 + auto it = m_volumes.find(Name);
210 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end());
211 +
212 + return it->second->Status();
213 }
214
215 WSLCVolumes::PruneVolumesResult WSLCVolumes::PruneVolumes(const std::map<std::string, std::vector<std::string>>& Filters)
@@ -245,7 +248,7 @@ WSLCVolumes::PruneVolumesResult WSLCVolumes::PruneVolumes(const std::map<std::st
248 catch (...)
249 {
250 LOG_CAUGHT_EXCEPTION_MSG("Failed to release host resources for pruned volume: %hs", name.c_str());
248 - EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcVolumeReleaseFailed(wsl::shared::string::MultiByteToWide(name)));
251 + EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcVolumeReleaseFailed(name));
252 }
253
254 m_volumes.erase(it);
src/windows/wslcsession/WSLCVolumes.h
+1 -1
@@ -52,7 +52,7 @@ public:
52
53 std::string InspectVolume(_In_ const std::string& Name) const;
54
55 - bool ContainsVolume(_In_ const std::string& Name) const;
55 + std::pair<HRESULT, std::string> GetVolumeStatus(_In_ const std::string& Name) const;
56
57 private:
58 __requires_lock_held(m_lock) void OpenVolumeExclusiveLockHeld(const wsl::windows::common::docker_schema::Volume& vol);
test/windows/WSLCTests.cpp
+87 -4
@@ -4198,11 +4198,31 @@ class WSLCTests
4198 VERIFY_ARE_EQUAL(error, std::error_code{});
4199 }
4200
4201 - wil::com_ptr<IWSLCContainer> notFound;
4202 - VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(containerName.c_str(), &notFound), E_UNEXPECTED);
4201 + // The container can still be opened even though its backing volume is gone, so the
4202 + // user is able to inspect and delete it.
4203 + wil::com_ptr<IWSLCContainer> recoveredContainer;
4204 + VERIFY_SUCCEEDED(m_defaultSession->OpenContainer(containerName.c_str(), &recoveredContainer));
4205 +
4206 + // Starting it must fail since the referenced volume cannot be brought online.
4207 + VERIFY_ARE_EQUAL(recoveredContainer->Start(WSLCContainerStartFlagsNone, nullptr, nullptr), WSLC_E_VOLUME_NOT_AVAILABLE);
4208 + ValidateCOMErrorMessageContains(wsl::shared::string::MultiByteToWide(volumeName));
4209 +
4210 + // Inspecting the volume reports the failure via an "Error" entry in its status.
4211 + {
4212 + wil::unique_cotaskmem_ansistring inspectOutput;
4213 + VERIFY_SUCCEEDED(m_defaultSession->InspectVolume(volumeName.c_str(), &inspectOutput));
4214 + auto inspect = wsl::shared::FromJson<wsl::windows::common::wslc_schema::InspectVolume>(inspectOutput.get());
4215 + VERIFY_IS_TRUE(inspect.Status.has_value());
4216 + VERIFY_IS_TRUE(inspect.Status->contains("Error"));
4217
4204 - // Deleting the named volume should fail since the volume was not recovered.
4205 - VERIFY_ARE_EQUAL(m_defaultSession->DeleteVolume(volumeName.c_str()), WSLC_E_VOLUME_NOT_FOUND);
4218 + // The backing .vhdx was deleted, so recovery fails to attach it with ERROR_FILE_NOT_FOUND.
4219 + const auto expectedError = wsl::shared::string::WideToMultiByte(GetErrorString(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)));
4220 + VERIFY_ARE_EQUAL(inspect.Status->at("Error"), expectedError);
4221 + }
4222 +
4223 + // The unavailable volume can still be deleted once the container referencing it is removed.
4224 + VERIFY_SUCCEEDED(recoveredContainer->Delete(WSLCDeleteFlagsForce));
4225 + VERIFY_SUCCEEDED(m_defaultSession->DeleteVolume(volumeName.c_str()));
4226 }
4227
4228 WSLC_TEST_METHOD(NamedVolumeGuestDriverOptsTest)
@@ -10767,4 +10787,67 @@ class WSLCTests
10787 VERIFY_SUCCEEDED(session->Terminate());
10788 }
10789 }
10790 +
10791 + WSLC_TEST_METHOD(WarningCallbackGuestVolumeRecovery)
10792 + {
10793 + SKIP_TEST_SERVER();
10794 +
10795 + constexpr auto c_sessionName = L"warning-guest-volume-recovery";
10796 + constexpr auto c_volumeName = "wslc-test-warning-guest-recovery";
10797 + auto storagePath = (std::filesystem::current_path() / "test-warning-guest-volume-recovery").wstring();
10798 + auto cleanupDir = wil::scope_exit([&]() {
10799 + std::error_code ec;
10800 + std::filesystem::remove_all(storagePath, ec);
10801 + });
10802 +
10803 + // Create a session and, via the docker CLI, inject a "local" volume with driver options we don't support (type=nfs).
10804 + // This bypasses our CreateVolume validation, leaving a volume that WSLCGuestVolumeImpl::Open will reject when the next
10805 + // session recovers it.
10806 + {
10807 + auto settings = GetDefaultSessionSettings(c_sessionName, false, WSLCNetworkingModeVirtioProxy);
10808 + settings.StoragePath = storagePath.c_str();
10809 + auto session = CreateSession(settings);
10810 +
10811 + ExpectCommandResult(
10812 + session.get(),
10813 + {"/usr/bin/docker",
10814 + "volume",
10815 + "create",
10816 + "--driver",
10817 + "local",
10818 + "--opt",
10819 + "type=nfs",
10820 + "--opt",
10821 + "o=addr=127.0.0.1,rw",
10822 + "--opt",
10823 + "device=:/exports/test",
10824 + c_volumeName},
10825 + 0);
10826 +
10827 + VERIFY_SUCCEEDED(session->Terminate());
10828 + }
10829 +
10830 + // Restart with a warning callback and verify the unsupported volume triggers a recovery warning when the session loads.
10831 + {
10832 + auto warningCallback = Microsoft::WRL::Make<CapturingWarningCallback>();
10833 +
10834 + auto settings = GetDefaultSessionSettings(c_sessionName, false, WSLCNetworkingModeVirtioProxy);
10835 + settings.StoragePath = storagePath.c_str();
10836 +
10837 + const auto sessionManager = OpenSessionManager();
10838 + wil::com_ptr<IWSLCSession> session;
10839 + VERIFY_SUCCEEDED(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, warningCallback.Get(), &session));
10840 + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get());
10841 +
10842 + auto warnings = warningCallback->GetWarnings();
10843 + auto expectedWarning = std::format(L"wsl: {}\n", wsl::shared::Localization::MessageWslcFailedToRecoverVolume(c_volumeName));
10844 +
10845 + VERIFY_IS_TRUE(std::ranges::any_of(warnings, [&](const auto& w) { return w == expectedWarning; }));
10846 +
10847 + // Clean up the volume from Docker's metadata.
10848 + ExpectCommandResult(session.get(), {"/usr/bin/docker", "volume", "rm", "-f", c_volumeName}, 0);
10849 +
10850 + VERIFY_SUCCEEDED(session->Terminate());
10851 + }
10852 + }
10853 };