Fix Lost sub directory structure when mapping to a VHD volume (#40857)
* Add hourly upstream sync workflow for feature/wsl-for-apps * Remove upstream sync workflow * Delete lost and found * Move test to wslc tests * Fix comment * Address feedback * Fix Localization --------- Co-authored-by: Kevin Vega <kevinve@microsoft.com>
Kevin Vega committed
Jul 2, 2026 at 11:45 UTC
6c7f97fcb9dd38ba6035a742022c2dde5a1ffe75
7 files changed
+177
-1
localization/strings/en-US/Resources.resw
+4
@@ -3370,6 +3370,10 @@ On first run, creates the file with all settings commented out at their defaults
3370
<value>Failed to unmount volume '{}': {}</value>
3371
<comment>{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3372
</data>
3373
+ <data name="MessageWslcVolumeLostFoundNotEmpty" xml:space="preserve">
3374
+ <value>Volume '{}' has a non-empty 'lost+found' directory. It was left in place and the volume may not be seeded with image contents.</value>
3375
+ <comment>{Locked="lost+found"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3376
+ </data>
3377
<data name="MessageWslcContainerStopAfterPluginRejectionFailed" xml:space="preserve">
3378
<value>Failed to stop container '{}' after plugin rejection</value>
3379
<comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
src/linux/init/WSLCInit.cpp
+38
-1
@@ -208,6 +208,43 @@ void HandleMessageImpl(
208
Transaction.Send<WSLC_GET_DISK::TResponse>(writer.Span());
209
}
210
211
+void HandleMessageImpl(
212
+ wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_LISTDIR& Message, const gsl::span<gsl::byte>& Buffer)
213
+{
214
+ wsl::shared::MessageWriter<WSLC_LISTDIR_RESULT> writer;
215
+
216
+ try
217
+ {
218
+ const auto* path = wsl::shared::string::FromMessageBuffer<WSLC_LISTDIR>(Buffer);
219
+ THROW_ERRNO_IF(EINVAL, path == nullptr);
220
+
221
+ wil::unique_dir dir{opendir(path)};
222
+ THROW_LAST_ERROR_IF(!dir);
223
+
224
+ std::vector<std::string> entries;
225
+ for (dirent64* entry = readdir64(dir.get()); entry != nullptr; entry = readdir64(dir.get()))
226
+ {
227
+ const std::string_view name{entry->d_name};
228
+ if (name == "." || name == "..")
229
+ {
230
+ continue;
231
+ }
232
+
233
+ entries.emplace_back(name);
234
+ }
235
+
236
+ auto pointers = wsl::shared::string::StringPointersFromArray(entries, false);
237
+ writer.WriteStringArray(writer->EntriesIndex, pointers.data(), pointers.size());
238
+ writer->Result = 0;
239
+ }
240
+ catch (...)
241
+ {
242
+ writer->Result = wil::ResultFromCaughtException();
243
+ }
244
+
245
+ Transaction.Send<WSLC_LISTDIR::TResponse>(writer.Span());
246
+}
247
+
248
void HandleMessageImpl(
249
wsl::shared::SocketChannel& Channel,
250
wsl::shared::Transaction& Transaction,
@@ -984,7 +1021,7 @@ void ProcessMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transactio
1021
{
1022
try
1023
{
987
- 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>(
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>(
1025
Channel, Transaction, Type, Buffer);
1026
}
1027
catch (...)
src/shared/inc/lxinitshared.h
+31
@@ -410,6 +410,8 @@ typedef enum _LX_MESSAGE_TYPE
410
LxMessageWSLCUnixConnect,
411
LxMessageWSLCGetGuestCapabilities,
412
LxMessageWSLCGetGuestCapabilitiesResult,
413
+ LxMessageWSLCListDir,
414
+ LxMessageWSLCListDirResult,
415
} LX_MESSAGE_TYPE,
416
*PLX_MESSAGE_TYPE;
417
@@ -522,6 +524,8 @@ inline auto ToString(LX_MESSAGE_TYPE messageType)
524
X(LxMessageWSLCUnixConnect)
525
X(LxMessageWSLCGetGuestCapabilities)
526
X(LxMessageWSLCGetGuestCapabilitiesResult)
527
+ X(LxMessageWSLCListDir)
528
+ X(LxMessageWSLCListDirResult)
529
530
default:
531
return "<unexpected LX_MESSAGE_TYPE>";
@@ -1585,6 +1589,33 @@ struct WSLC_GET_DISK
1589
PRETTY_PRINT(FIELD(Header), FIELD(ScsiLun));
1590
};
1591
1592
+struct WSLC_LISTDIR_RESULT
1593
+{
1594
+ static inline auto Type = LxMessageWSLCListDirResult;
1595
+
1596
+ DECLARE_MESSAGE_CTOR(WSLC_LISTDIR_RESULT);
1597
+
1598
+ MESSAGE_HEADER Header;
1599
+ int Result{};
1600
+ unsigned int EntriesIndex{};
1601
+ char Buffer[];
1602
+
1603
+ PRETTY_PRINT(FIELD(Header), FIELD(Result), STRING_ARRAY_FIELD(EntriesIndex));
1604
+};
1605
+
1606
+struct WSLC_LISTDIR
1607
+{
1608
+ static inline auto Type = LxMessageWSLCListDir;
1609
+ using TResponse = WSLC_LISTDIR_RESULT;
1610
+
1611
+ DECLARE_MESSAGE_CTOR(WSLC_LISTDIR);
1612
+
1613
+ MESSAGE_HEADER Header;
1614
+ char Buffer[];
1615
+
1616
+ PRETTY_PRINT(FIELD(Header), FIELD(Buffer));
1617
+};
1618
+
1619
struct WSLC_MOUNT_RESULT
1620
{
1621
static inline auto Type = LxMessageWSLCMountResult;
src/windows/wslcsession/WSLCVhdVolume.cpp
+34
@@ -84,6 +84,31 @@ namespace {
84
return name;
85
}
86
87
+ void RemoveLostFoundDirectory(WSLCVirtualMachine& VirtualMachine, const std::string& VolumeName, const std::string& MountPath)
88
+ try
89
+ {
90
+ constexpr auto c_lostFoundDir = "lost+found";
91
+ const auto entries = VirtualMachine.ListDirectory(MountPath);
92
+
93
+ // Only remove lost+found if the disk is empty besides that directory.
94
+ if (entries.size() != 1 || entries.front() != c_lostFoundDir)
95
+ {
96
+ return;
97
+ }
98
+
99
+ try
100
+ {
101
+ VirtualMachine.RemoveDirectory(std::format("{}/{}", MountPath, c_lostFoundDir));
102
+ }
103
+ catch (...)
104
+ {
105
+ // rmdir only removes an empty directory, so reaching here means the
106
+ // lone lost+found captured recovered data. Leave it and warn.
107
+ LOG_CAUGHT_EXCEPTION();
108
+ EMIT_USER_WARNING(Localization::MessageWslcVolumeLostFoundNotEmpty(VolumeName));
109
+ }
110
+ }
111
+ CATCH_LOG();
112
} // namespace
113
114
WSLCVhdVolumeImpl::WSLCVhdVolumeImpl(
@@ -151,6 +176,13 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Create(
176
177
auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
178
179
+ // mkfs.ext4 always creates a lost+found directory at the filesystem root,
180
+ // which makes a freshly formatted volume look non-empty to Docker and
181
+ // suppresses the copy-up that seeds image data on first use. Drop it so
182
+ // Docker seeds the volume with the image's contents. No-op when the volume
183
+ // already contains data.
184
+ RemoveLostFoundDirectory(VirtualMachine, name, virtualMachinePath);
185
+
186
WSLCVolumeMetadata metadata;
187
metadata.Driver = WSLCVhdVolumeDriver;
188
metadata.DriverOpts = DriverOpts;
@@ -246,6 +278,8 @@ std::unique_ptr<WSLCVhdVolumeImpl> WSLCVhdVolumeImpl::Open(
278
VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "", 0);
279
auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
280
281
+ RemoveLostFoundDirectory(VirtualMachine, Volume.Name, virtualMachinePath);
282
+
283
lun = attachedLun;
284
attached = true;
285
src/windows/wslcsession/WSLCVirtualMachine.cpp
+27
@@ -560,6 +560,33 @@ void WSLCVirtualMachine::Ext4Format(const std::string& Device, std::optional<uin
560
THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
561
}
562
563
+void WSLCVirtualMachine::RemoveDirectory(const std::string& Path)
564
+{
565
+ // rmdir only removes an empty directory, so callers can rely on it to leave
566
+ // a non-empty directory untouched.
567
+ constexpr auto rmdirPath = "/bin/rmdir";
568
+
569
+ std::vector<std::string> args = {rmdirPath, Path};
570
+
571
+ ServiceProcessLauncher launcher(rmdirPath, args);
572
+ auto result = launcher.Launch(*this).WaitAndCaptureOutput();
573
+
574
+ THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
575
+}
576
+
577
+std::vector<std::string> WSLCVirtualMachine::ListDirectory(const std::string& Path)
578
+{
579
+ wsl::shared::MessageWriter<WSLC_LISTDIR> message;
580
+ message.WriteString(Path);
581
+
582
+ gsl::span<gsl::byte> responseSpan;
583
+ const auto& response = m_initChannel.Transaction<WSLC_LISTDIR>(message.Span(), &responseSpan, m_initChannelTimeout);
584
+
585
+ THROW_HR_IF_MSG(E_FAIL, response.Result != 0, "Failed to list directory '%hs', init returned: %d", Path.c_str(), response.Result);
586
+
587
+ return wsl::shared::string::ArrayFromSpan(responseSpan, response.EntriesIndex);
588
+}
589
+
590
void WSLCVirtualMachine::Unmount(_In_ const char* Path)
591
{
592
auto [pid, _, subChannel] = Fork(WSLC_FORK::Thread);
src/windows/wslcsession/WSLCVirtualMachine.h
+2
@@ -160,6 +160,8 @@ public:
160
void DetachDisk(_In_ ULONG Lun);
161
void Ext4Format(_In_ const std::string& Device, _In_ std::optional<uint32_t> Uid = std::nullopt, _In_ std::optional<uint32_t> Gid = std::nullopt);
162
void Mount(_In_ LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags);
163
+ void RemoveDirectory(_In_ const std::string& Path);
164
+ std::vector<std::string> ListDirectory(_In_ const std::string& Path);
165
166
wil::unique_socket ConnectUnixSocket(_In_ const char* Path);
167
std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> Fork(enum WSLC_FORK::ForkType Type);
test/windows/WSLCTests.cpp
+41
@@ -4165,6 +4165,47 @@ class WSLCTests
4165
VERIFY_IS_FALSE(std::filesystem::exists(volumeVhdPath));
4166
}
4167
4168
+ WSLC_TEST_METHOD(NamedVolumesVhdSeedsImageData)
4169
+ {
4170
+ // A freshly formatted VHD volume must be seeded with the image's content
4171
+ // on first use, just like a guest volume. mkfs.ext4 creates a lost+found
4172
+ // directory at the volume root; if it isn't removed, Docker treats the
4173
+ // volume as non-empty and skips the copy-up that seeds image data.
4174
+ // Mounting the empty volume over a directory the image is guaranteed to
4175
+ // populate (/etc) exercises that copy-up.
4176
+ WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
4177
+ const std::string volumeName = "wslc-test-named-volume-vhd-seed";
4178
+
4179
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
4180
+
4181
+ WSLCVolumeOptions volumeOptions{};
4182
+ volumeOptions.Name = volumeName.c_str();
4183
+ volumeOptions.Driver = "vhd";
4184
+ volumeOptions.DriverOpts = driverOpts;
4185
+ volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
4186
+
4187
+ WSLCVolumeInformation volInfo{};
4188
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
4189
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
4190
+
4191
+ WSLCContainerLauncher launcher("debian:latest", "wslc-vhd-seed-container", {"/bin/sh", "-c", "ls -A /etc"});
4192
+ launcher.AddNamedVolume(volumeName, "/etc", false);
4193
+
4194
+ auto container = launcher.Launch(*m_defaultSession);
4195
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
4196
+
4197
+ VERIFY_ARE_EQUAL(0, result.Code);
4198
+
4199
+ // Image content was seeded into the volume...
4200
+ VERIFY_IS_TRUE(
4201
+ result.Output[1].find("passwd") != std::string::npos,
4202
+ L"Image's /etc content should be seeded into the fresh VHD volume");
4203
+
4204
+ // ...and the ext4 lost+found is gone, so it never blocked copy-up.
4205
+ VERIFY_IS_TRUE(
4206
+ result.Output[1].find("lost+found") == std::string::npos, L"lost+found should have been removed from the volume root");
4207
+ }
4208
+
4209
WSLC_TEST_METHOD(NamedVolumesGuest)
4210
{
4211
ValidateNamedVolumeContract("guest", nullptr, 0);