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(), ¬Found), 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
};