diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw
index 974bf046c8..cd04db8282 100644
--- a/localization/strings/en-US/Resources.resw
+++ b/localization/strings/en-US/Resources.resw
@@ -3316,6 +3316,10 @@ On first run, creates the file with all settings commented out at their defaults
Failed to unmount volume '{}': {}
{FixedPlaceholder="{}"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
+ Volume '{}' has a non-empty 'lost+found' directory. It was left in place and the volume may not be seeded with image contents.
+ {Locked="lost+found"}{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
+
Failed to stop container '{}' after plugin rejection
{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated
diff --git a/src/linux/init/WSLCInit.cpp b/src/linux/init/WSLCInit.cpp
index 6afecc8102..01f189244c 100644
--- a/src/linux/init/WSLCInit.cpp
+++ b/src/linux/init/WSLCInit.cpp
@@ -189,6 +189,43 @@ void HandleMessageImpl(
Transaction.Send(writer.Span());
}
+void HandleMessageImpl(
+ wsl::shared::SocketChannel& Channel, wsl::shared::Transaction& Transaction, const WSLC_LISTDIR& Message, const gsl::span& Buffer)
+{
+ wsl::shared::MessageWriter writer;
+
+ try
+ {
+ const auto* path = wsl::shared::string::FromMessageBuffer(Buffer);
+ THROW_ERRNO_IF(EINVAL, path == nullptr);
+
+ wil::unique_dir dir{opendir(path)};
+ THROW_LAST_ERROR_IF(!dir);
+
+ std::vector entries;
+ for (dirent64* entry = readdir64(dir.get()); entry != nullptr; entry = readdir64(dir.get()))
+ {
+ const std::string_view name{entry->d_name};
+ if (name == "." || name == "..")
+ {
+ continue;
+ }
+
+ entries.emplace_back(name);
+ }
+
+ auto pointers = wsl::shared::string::StringPointersFromArray(entries, false);
+ writer.WriteStringArray(writer->EntriesIndex, pointers.data(), pointers.size());
+ writer->Result = 0;
+ }
+ catch (...)
+ {
+ writer->Result = wil::ResultFromCaughtException();
+ }
+
+ Transaction.Send(writer.Span());
+}
+
void HandleMessageImpl(
wsl::shared::SocketChannel& Channel,
wsl::shared::Transaction& Transaction,
@@ -952,7 +989,7 @@ void ProcessMessage(wsl::shared::SocketChannel& Channel, wsl::shared::Transactio
{
try
{
- HandleMessage(
+ HandleMessage(
Channel, Transaction, Type, Buffer);
}
catch (...)
diff --git a/src/shared/inc/lxinitshared.h b/src/shared/inc/lxinitshared.h
index 9c9e3fde9f..9c56464daa 100644
--- a/src/shared/inc/lxinitshared.h
+++ b/src/shared/inc/lxinitshared.h
@@ -410,6 +410,8 @@ typedef enum _LX_MESSAGE_TYPE
LxMessageWSLCUnixConnect,
LxMessageWSLCGetGuestCapabilities,
LxMessageWSLCGetGuestCapabilitiesResult,
+ LxMessageWSLCListDir,
+ LxMessageWSLCListDirResult,
} LX_MESSAGE_TYPE,
*PLX_MESSAGE_TYPE;
@@ -522,6 +524,8 @@ inline auto ToString(LX_MESSAGE_TYPE messageType)
X(LxMessageWSLCUnixConnect)
X(LxMessageWSLCGetGuestCapabilities)
X(LxMessageWSLCGetGuestCapabilitiesResult)
+ X(LxMessageWSLCListDir)
+ X(LxMessageWSLCListDirResult)
default:
return "";
@@ -1585,6 +1589,33 @@ struct WSLC_GET_DISK
PRETTY_PRINT(FIELD(Header), FIELD(ScsiLun));
};
+struct WSLC_LISTDIR_RESULT
+{
+ static inline auto Type = LxMessageWSLCListDirResult;
+
+ DECLARE_MESSAGE_CTOR(WSLC_LISTDIR_RESULT);
+
+ MESSAGE_HEADER Header;
+ int Result{};
+ unsigned int EntriesIndex{};
+ char Buffer[];
+
+ PRETTY_PRINT(FIELD(Header), FIELD(Result), STRING_ARRAY_FIELD(EntriesIndex));
+};
+
+struct WSLC_LISTDIR
+{
+ static inline auto Type = LxMessageWSLCListDir;
+ using TResponse = WSLC_LISTDIR_RESULT;
+
+ DECLARE_MESSAGE_CTOR(WSLC_LISTDIR);
+
+ MESSAGE_HEADER Header;
+ char Buffer[];
+
+ PRETTY_PRINT(FIELD(Header), FIELD(Buffer));
+};
+
struct WSLC_MOUNT_RESULT
{
static inline auto Type = LxMessageWSLCMountResult;
diff --git a/src/windows/wslcsession/WSLCVhdVolume.cpp b/src/windows/wslcsession/WSLCVhdVolume.cpp
index d194bd6970..43fc783698 100644
--- a/src/windows/wslcsession/WSLCVhdVolume.cpp
+++ b/src/windows/wslcsession/WSLCVhdVolume.cpp
@@ -84,6 +84,31 @@ namespace {
return name;
}
+ void RemoveLostFoundDirectory(WSLCVirtualMachine& VirtualMachine, const std::string& VolumeName, const std::string& MountPath)
+ try
+ {
+ constexpr auto c_lostFoundDir = "lost+found";
+ const auto entries = VirtualMachine.ListDirectory(MountPath);
+
+ // Only remove lost+found if the disk is empty besides that directory.
+ if (entries.size() != 1 || entries.front() != c_lostFoundDir)
+ {
+ return;
+ }
+
+ try
+ {
+ VirtualMachine.RemoveDirectory(std::format("{}/{}", MountPath, c_lostFoundDir));
+ }
+ catch (...)
+ {
+ // rmdir only removes an empty directory, so reaching here means the
+ // lone lost+found captured recovered data. Leave it and warn.
+ LOG_CAUGHT_EXCEPTION();
+ EMIT_USER_WARNING(Localization::MessageWslcVolumeLostFoundNotEmpty(VolumeName));
+ }
+ }
+ CATCH_LOG();
} // namespace
WSLCVhdVolumeImpl::WSLCVhdVolumeImpl(
@@ -151,6 +176,13 @@ std::unique_ptr WSLCVhdVolumeImpl::Create(
auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
+ // mkfs.ext4 always creates a lost+found directory at the filesystem root,
+ // which makes a freshly formatted volume look non-empty to Docker and
+ // suppresses the copy-up that seeds image data on first use. Drop it so
+ // Docker seeds the volume with the image's contents. No-op when the volume
+ // already contains data.
+ RemoveLostFoundDirectory(VirtualMachine, name, virtualMachinePath);
+
WSLCVolumeMetadata metadata;
metadata.Driver = WSLCVhdVolumeDriver;
metadata.DriverOpts = DriverOpts;
@@ -246,6 +278,8 @@ std::unique_ptr WSLCVhdVolumeImpl::Open(
VirtualMachine.Mount(device.c_str(), virtualMachinePath.c_str(), "ext4", "", 0);
auto mountCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { VirtualMachine.Unmount(virtualMachinePath.c_str()); });
+ RemoveLostFoundDirectory(VirtualMachine, Volume.Name, virtualMachinePath);
+
lun = attachedLun;
attached = true;
diff --git a/src/windows/wslcsession/WSLCVirtualMachine.cpp b/src/windows/wslcsession/WSLCVirtualMachine.cpp
index 77a09e1ad8..a360f7d1e4 100644
--- a/src/windows/wslcsession/WSLCVirtualMachine.cpp
+++ b/src/windows/wslcsession/WSLCVirtualMachine.cpp
@@ -536,6 +536,33 @@ void WSLCVirtualMachine::Ext4Format(const std::string& Device, std::optional args = {rmdirPath, Path};
+
+ ServiceProcessLauncher launcher(rmdirPath, args);
+ auto result = launcher.Launch(*this).WaitAndCaptureOutput();
+
+ THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
+}
+
+std::vector WSLCVirtualMachine::ListDirectory(const std::string& Path)
+{
+ wsl::shared::MessageWriter message;
+ message.WriteString(Path);
+
+ gsl::span responseSpan;
+ const auto& response = m_initChannel.Transaction(message.Span(), &responseSpan, m_initChannelTimeout);
+
+ THROW_HR_IF_MSG(E_FAIL, response.Result != 0, "Failed to list directory '%hs', init returned: %d", Path.c_str(), response.Result);
+
+ return wsl::shared::string::ArrayFromSpan(responseSpan, response.EntriesIndex);
+}
+
void WSLCVirtualMachine::Unmount(_In_ const char* Path)
{
auto [pid, _, subChannel] = Fork(WSLC_FORK::Thread);
diff --git a/src/windows/wslcsession/WSLCVirtualMachine.h b/src/windows/wslcsession/WSLCVirtualMachine.h
index f6e438c5c0..2a2cccad2d 100644
--- a/src/windows/wslcsession/WSLCVirtualMachine.h
+++ b/src/windows/wslcsession/WSLCVirtualMachine.h
@@ -159,6 +159,8 @@ class WSLCVirtualMachine
void DetachDisk(_In_ ULONG Lun);
void Ext4Format(_In_ const std::string& Device, _In_ std::optional Uid = std::nullopt, _In_ std::optional Gid = std::nullopt);
void Mount(_In_ LPCSTR Source, _In_ LPCSTR Target, _In_ LPCSTR Type, _In_ LPCSTR Options, _In_ ULONG Flags);
+ void RemoveDirectory(_In_ const std::string& Path);
+ std::vector ListDirectory(_In_ const std::string& Path);
wil::unique_socket ConnectUnixSocket(_In_ const char* Path);
std::tuple Fork(enum WSLC_FORK::ForkType Type);
diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp
index b0cde34fb2..1c4c486b81 100644
--- a/test/windows/WSLCTests.cpp
+++ b/test/windows/WSLCTests.cpp
@@ -4003,6 +4003,47 @@ class WSLCTests
VERIFY_IS_FALSE(std::filesystem::exists(volumeVhdPath));
}
+ WSLC_TEST_METHOD(NamedVolumesVhdSeedsImageData)
+ {
+ // A freshly formatted VHD volume must be seeded with the image's content
+ // on first use, just like a guest volume. mkfs.ext4 creates a lost+found
+ // directory at the volume root; if it isn't removed, Docker treats the
+ // volume as non-empty and skips the copy-up that seeds image data.
+ // Mounting the empty volume over a directory the image is guaranteed to
+ // populate (/etc) exercises that copy-up.
+ WSLCDriverOption driverOpts[] = {{"SizeBytes", "1073741824"}};
+ const std::string volumeName = "wslc-test-named-volume-vhd-seed";
+
+ LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str()));
+
+ WSLCVolumeOptions volumeOptions{};
+ volumeOptions.Name = volumeName.c_str();
+ volumeOptions.Driver = "vhd";
+ volumeOptions.DriverOpts = driverOpts;
+ volumeOptions.DriverOptsCount = ARRAYSIZE(driverOpts);
+
+ WSLCVolumeInformation volInfo{};
+ VERIFY_SUCCEEDED(m_defaultSession->CreateVolume(&volumeOptions, &volInfo));
+ auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(m_defaultSession->DeleteVolume(volumeName.c_str())); });
+
+ WSLCContainerLauncher launcher("debian:latest", "wslc-vhd-seed-container", {"/bin/sh", "-c", "ls -A /etc"});
+ launcher.AddNamedVolume(volumeName, "/etc", false);
+
+ auto container = launcher.Launch(*m_defaultSession);
+ auto result = container.GetInitProcess().WaitAndCaptureOutput();
+
+ VERIFY_ARE_EQUAL(0, result.Code);
+
+ // Image content was seeded into the volume...
+ VERIFY_IS_TRUE(
+ result.Output[1].find("passwd") != std::string::npos,
+ L"Image's /etc content should be seeded into the fresh VHD volume");
+
+ // ...and the ext4 lost+found is gone, so it never blocked copy-up.
+ VERIFY_IS_TRUE(
+ result.Output[1].find("lost+found") == std::string::npos, L"lost+found should have been removed from the volume root");
+ }
+
WSLC_TEST_METHOD(NamedVolumesGuest)
{
ValidateNamedVolumeContract("guest", nullptr, 0);