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