@samitouri / QOSAMI-WSL / commits / 6c7f97fc

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);