From 7d41e66211386f3f8dffb07f13e6b62f8d723fa8 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 11:15:35 -0700 Subject: [PATCH 01/76] Add local registry, auth, and push image --- src/windows/common/CMakeLists.txt | 5 + src/windows/common/WSLCContainerLauncher.h | 1 + src/windows/common/WSLCLocalRegistry.cpp | 125 +++++++++++++++++ src/windows/common/WSLCLocalRegistry.h | 53 +++++++ src/windows/common/WslcCredentialStore.cpp | 43 ++++++ src/windows/common/WslcCredentialStore.h | 24 ++++ src/windows/inc/docker_schema.h | 21 ++- src/windows/service/inc/wslc.idl | 2 + src/windows/wslcsession/DockerHTTPClient.cpp | 47 ++++++- src/windows/wslcsession/DockerHTTPClient.h | 6 +- src/windows/wslcsession/WSLCContainer.cpp | 12 +- src/windows/wslcsession/WSLCSession.cpp | 138 ++++++++++++++----- src/windows/wslcsession/WSLCSession.h | 3 + test/windows/CMakeLists.txt | 3 +- test/windows/WSLCTests.cpp | 48 +++++++ 15 files changed, 489 insertions(+), 42 deletions(-) create mode 100644 src/windows/common/WSLCLocalRegistry.cpp create mode 100644 src/windows/common/WSLCLocalRegistry.h create mode 100644 src/windows/common/WslcCredentialStore.cpp create mode 100644 src/windows/common/WslcCredentialStore.h diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index 5693497d2..503bd89ea 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -35,6 +35,8 @@ set(SOURCES SubProcess.cpp svccomm.cpp WSLCContainerLauncher.cpp + WSLCLocalRegistry.cpp + WslcCredentialStore.cpp VirtioNetworking.cpp WSLCProcessLauncher.cpp WslClient.cpp @@ -116,6 +118,8 @@ set(HEADERS SubProcess.h svccomm.hpp WSLCContainerLauncher.h + WSLCLocalRegistry.h + WslcCredentialStore.h VirtioNetworking.h WSLCProcessLauncher.h WslClient.h @@ -134,6 +138,7 @@ set(HEADERS add_library(common STATIC ${SOURCES} ${HEADERS}) add_dependencies(common wslserviceidl wslcidl localization wslservicemc wslinstalleridl) +target_link_libraries(common PRIVATE Crypt32.lib) target_precompile_headers(common PRIVATE precomp.h) set_target_properties(common PROPERTIES FOLDER windows) diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index 295e3b33d..f0191d6a5 100644 --- a/src/windows/common/WSLCContainerLauncher.h +++ b/src/windows/common/WSLCContainerLauncher.h @@ -79,6 +79,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher void SetDnsSearchDomains(std::vector&& DnsSearchDomains); void SetDnsOptions(std::vector&& DnsOptions); + using WSLCProcessLauncher::FormatResult; using WSLCProcessLauncher::SetUser; using WSLCProcessLauncher::SetWorkingDirectory; diff --git a/src/windows/common/WSLCLocalRegistry.cpp b/src/windows/common/WSLCLocalRegistry.cpp new file mode 100644 index 000000000..2340bc4f2 --- /dev/null +++ b/src/windows/common/WSLCLocalRegistry.cpp @@ -0,0 +1,125 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCLocalRegistry.cpp + +Abstract: + + Implementation of WSLCLocalRegistry. + +--*/ +#include "WSLCLocalRegistry.h" + +using wsl::windows::common::RunningWSLCContainer; +using wsl::windows::common::WSLCContainerLauncher; +using wsl::windows::common::WSLCLocalRegistry; + +namespace { + +constexpr auto c_registryImage = "registry:3"; +constexpr auto c_htpasswdImage = "httpd:2"; + +void PopulateHtpasswd(IWSLCSession& session, const std::filesystem::path& storagePath, const std::string& username, const std::string& password) +{ + LOG_IF_FAILED(session.PullImage(c_htpasswdImage, nullptr, nullptr)); + + // Write the htpasswd file into /data/ on the shared folder. + const auto command = std::format("htpasswd -Bbn '{}' '{}' > /data/htpasswd", username, password); + + WSLCContainerLauncher launcher("httpd:2", {}, {"/bin/sh", "-c", command}); + launcher.AddVolume(storagePath.wstring(), "/data", false); + launcher.SetContainerFlags(WSLCContainerFlagsRm); + + auto container = launcher.Launch(session); + auto result = container.GetInitProcess().WaitAndCaptureOutput(); + + THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str()); +} + +std::vector BuildRegistryEnv(bool withAuth) +{ + // Builds the environment variable list for the registry:3 container. + std::vector env = { + "REGISTRY_HTTP_ADDR=0.0.0.0:5000", + "REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/data/registry", + "REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io" + }; + + if (withAuth) + { + env.push_back("REGISTRY_AUTH=htpasswd"); + env.push_back("REGISTRY_AUTH_HTPASSWD_REALM=WSLC Test Registry"); + env.push_back("REGISTRY_AUTH_HTPASSWD_PATH=/data/htpasswd"); + } + + return env; +} + +} // namespace + +WSLCLocalRegistry::WSLCLocalRegistry( + IWSLCSession& session, + RunningWSLCContainer&& container, + std::string&& username, + std::string&& password) : + m_session(wil::com_ptr(&session)), + m_username(std::move(username)), + m_password(std::move(password)), + m_container(std::move(container)) +{ +} + +WSLCLocalRegistry::~WSLCLocalRegistry() +{ + // Delete the container first while the session is still active. + m_container.Reset(); +} + +WSLCLocalRegistry WSLCLocalRegistry::Start( + IWSLCSession& session, const std::filesystem::path& storagePath, const std::string& username, const std::string& password) +{ + // Ensure required images are available. + LOG_IF_FAILED(session.PullImage(c_registryImage, nullptr, nullptr)); + + // If credentials are provided, populate the htpasswd file on the folder. + if (!username.empty()) + { + PopulateHtpasswd(session, storagePath, username, password); + } + + // Launch the registry container. + WSLCContainerLauncher launcher( + c_registryImage, + {}, + {}, + BuildRegistryEnv(!username.empty())); + + launcher.AddPort(5000, 5000, AF_INET); + launcher.AddVolume(storagePath.wstring(), "/data", false); + + auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); + + return WSLCLocalRegistry( + session, + std::move(container), + std::string(username), + std::string(password)); +} + +const char* WSLCLocalRegistry::GetServerAddress() const +{ + return "127.0.0.1:5000"; +} + +const std::string& WSLCLocalRegistry::GetUsername() const +{ + return m_username; +} + +const std::string& WSLCLocalRegistry::GetPassword() const +{ + return m_password; +} diff --git a/src/windows/common/WSLCLocalRegistry.h b/src/windows/common/WSLCLocalRegistry.h new file mode 100644 index 000000000..d61c4d717 --- /dev/null +++ b/src/windows/common/WSLCLocalRegistry.h @@ -0,0 +1,53 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCLocalRegistry.h + +Abstract: + + Helper class that starts a local Docker registry:3 container inside a WSLC + session, optionally configured with htpasswd basic authentication. Intended + for use in both unit tests and E2E tests that need a private registry without + an external dependency. + +--*/ + +#pragma once +#include "WSLCContainerLauncher.h" + +namespace wsl::windows::common { + +class WSLCLocalRegistry +{ +public: + NON_COPYABLE(WSLCLocalRegistry); + DEFAULT_MOVABLE(WSLCLocalRegistry); + ~WSLCLocalRegistry(); + + static WSLCLocalRegistry Start( + IWSLCSession& Session, + const std::filesystem::path& StoragePath, + const std::string& Username = {}, + const std::string& Password = {}); + + const char* GetServerAddress() const; + const std::string& GetUsername() const; + const std::string& GetPassword() const; + +private: + WSLCLocalRegistry( + IWSLCSession& session, + RunningWSLCContainer&& container, + std::string&& username, + std::string&& password); + + wil::com_ptr m_session; + std::string m_username; + std::string m_password; + RunningWSLCContainer m_container; +}; + +} // namespace wsl::windows::common diff --git a/src/windows/common/WslcCredentialStore.cpp b/src/windows/common/WslcCredentialStore.cpp new file mode 100644 index 000000000..3c5d35f70 --- /dev/null +++ b/src/windows/common/WslcCredentialStore.cpp @@ -0,0 +1,43 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WslcCredentialStore.cpp + +Abstract: + + Implementation of credential store helpers. + +--*/ + +#include "WslcCredentialStore.h" +#include +#include + +std::string wsl::windows::common::BuildRegistryAuthHeader( + const std::string& username, const std::string& password, const std::string& serverAddress) +{ + auto authJson = std::format( + R"({{"username":"{}","password":"{}","serveraddress":"{}"}})", username, password, serverAddress); + + DWORD base64Size = 0; + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( + reinterpret_cast(authJson.c_str()), + static_cast(authJson.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, + nullptr, + &base64Size)); + + std::string result(base64Size, '\0'); + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( + reinterpret_cast(authJson.c_str()), + static_cast(authJson.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, + result.data(), + &base64Size)); + + result.resize(base64Size); + return result; +} diff --git a/src/windows/common/WslcCredentialStore.h b/src/windows/common/WslcCredentialStore.h new file mode 100644 index 000000000..59b876577 --- /dev/null +++ b/src/windows/common/WslcCredentialStore.h @@ -0,0 +1,24 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WslcCredentialStore.h + +Abstract: + + Helpers for building Docker/OCI registry credential payloads. + +--*/ + +#pragma once +#include + +namespace wsl::windows::common { + +// Builds the base64-encoded X-Registry-Auth header value used by Docker APIs +// (PullImage, PushImage, etc.) from the given credentials. +std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress); + +} // namespace wsl::windows::common diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index 05cee1da0..a0d323fea 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -48,6 +48,25 @@ struct EmptyRequest using TResponse = void; }; +struct AuthRequest +{ + using TResponse = struct AuthResponse; + + std::string username; + std::string password; + std::string serveraddress; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(AuthRequest, username, password, serveraddress); +}; + +struct AuthResponse +{ + std::string Status; + std::optional IdentityToken; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(AuthResponse, Status, IdentityToken); +}; + struct CreateVolume { using TResponse = void; @@ -146,7 +165,7 @@ struct CreateContainer std::optional StopSignal; std::optional WorkingDir; std::vector Cmd; - std::vector Entrypoint; // TODO: Find a way to omit if the caller wants the default entrypoint. + std::optional> Entrypoint; std::vector Env; std::map ExposedPorts; std::map Labels; diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index d3b92eab3..b1b7fc55f 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -572,7 +572,9 @@ interface IWSLCSession : IUnknown HRESULT ListImages([in, unique] const WSLCListImageOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count); HRESULT DeleteImage([in] const WSLCDeleteImageOptions* Options, [out, size_is(, *Count)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* Count); HRESULT TagImage([in] const WSLCTagImageOptions* Options); + HRESULT PushImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); HRESULT InspectImage([in] LPCSTR ImageNameOrId, [out] LPSTR* Output); + HRESULT Authenticate([in] LPCSTR ServerAddress, [in] LPCSTR Username, [in] LPCSTR Password, [out] LPSTR* IdentityToken); // Container management. HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [out] IWSLCContainer** Container); diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index f2cf148c0..5aacd58e8 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -118,7 +118,7 @@ DockerHTTPClient::DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE { } -std::unique_ptr DockerHTTPClient::PullImage(const std::string& Repo, const std::optional& tagOrDigest) +std::unique_ptr DockerHTTPClient::PullImage(const std::string& Repo, const std::optional& tagOrDigest, const std::optional& registryAuth) { auto url = URL::Create("/images/create"); @@ -131,7 +131,14 @@ std::unique_ptr DockerHTTPClient::PullImag url.SetParameter("tag", tagOrDigest.value()); } - return SendRequestImpl(verb::post, url, {}, {}); + std::map customHeaders; + + if (registryAuth.has_value()) + { + customHeaders["X-Registry-Auth"] = registryAuth.value(); + } + + return SendRequestImpl(verb::post, url, {}, {}, customHeaders); } std::unique_ptr DockerHTTPClient::LoadImage(uint64_t ContentLength) @@ -163,6 +170,33 @@ void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, Transaction(verb::post, url); } +std::unique_ptr DockerHTTPClient::PushImage(const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth) +{ + auto url = URL::Create("/images/{}/push", ImageName); + + if (tag.has_value()) + { + url.SetParameter("tag", tag.value()); + } + + std::map customHeaders; + + if (registryAuth.has_value()) + { + customHeaders["X-Registry-Auth"] = registryAuth.value(); + } + + return SendRequestImpl(verb::post, url, {}, {}, customHeaders); +} + +std::string DockerHTTPClient::Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password) +{ + auto response = Transaction( + verb::post, URL::Create("/auth"), {.username = username, .password = password, .serveraddress = serverAddress}); + + return response.IdentityToken.value_or(""); +} + std::vector DockerHTTPClient::ListImages(bool all, bool digests, const ListImagesFilters& filters) { auto url = URL::Create("/images/json"); @@ -619,7 +653,7 @@ void DockerHTTPClient::DockerHttpResponseHandle::OnResponseBytes(const gsl::span } std::unique_ptr DockerHTTPClient::SendRequestImpl( - verb Method, const URL& Url, const std::string& Body, const std::map& Headers) + verb Method, const URL& Url, const std::string& Body, const std::map& Headers, const std::map& CustomHeaders) { auto context = std::make_unique(ConnectSocket()); @@ -637,11 +671,16 @@ std::unique_ptr DockerHTTPClient::SendRequ req.set(http::field::connection, "close"); req.set(http::field::accept, "application/json"); - for (const auto [field, value] : Headers) + for (const auto& [field, value] : Headers) { req.set(field, value); } + for (const auto& [name, value] : CustomHeaders) + { + req.set(name, value); + } + http::write(context->stream, req); #ifdef WSLC_HTTP_DEBUG diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index 309441a5d..e577bb675 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -151,10 +151,12 @@ class DockerHTTPClient std::vector labels; }; - std::unique_ptr PullImage(const std::string& Repo, const std::optional& tagOrDigest); + std::unique_ptr PullImage(const std::string& Repo, const std::optional& tagOrDigest, const std::optional& registryAuth = std::nullopt); std::unique_ptr ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength); std::unique_ptr LoadImage(uint64_t ContentLength); void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag); + std::unique_ptr PushImage(const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth = std::nullopt); + std::string Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password); std::vector ListImages(bool all = false, bool digests = false, const ListImagesFilters& filters = {}); common::docker_schema::InspectImage InspectImage(const std::string& NameOrId); std::vector DeleteImage(const char* Image, bool Force, bool NoPrune); // Image can be ID or Repo:Tag. @@ -224,7 +226,7 @@ class DockerHTTPClient wil::unique_socket ConnectSocket(); std::unique_ptr SendRequestImpl( - boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map& Headers); + boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map& Headers = {}, const std::map& CustomHeaders = {}); std::pair SendRequestAndReadResponse( boost::beast::http::verb Method, const URL& Url, const std::string& Body = ""); diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 01b9c756c..c159cf382 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1016,8 +1016,16 @@ std::unique_ptr WSLCContainerImpl::Create( request.OpenStdin = true; } - request.Cmd = StringArrayToVector(containerOptions.InitProcessOptions.CommandLine); - request.Entrypoint = StringArrayToVector(containerOptions.Entrypoint); + if (containerOptions.InitProcessOptions.CommandLine.Count > 0) + { + request.Cmd = StringArrayToVector(containerOptions.InitProcessOptions.CommandLine); + } + + if (containerOptions.Entrypoint.Count > 0) + { + request.Entrypoint = StringArrayToVector(containerOptions.Entrypoint); + } + request.Env = StringArrayToVector(containerOptions.InitProcessOptions.Environment); if (containerOptions.StopSignal != WSLCSignalNone) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index ababcf6f8..5e0ab1fe8 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -456,26 +456,9 @@ void WSLCSession::StartDockerd() m_dockerdProcess->GetExitEvent(), std::bind(&WSLCSession::OnDockerdExited, this))); } -HRESULT WSLCSession::PullImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback) -try +void WSLCSession::StreamImageOperation( + DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback) { - COMServiceExecutionContext context; - - RETURN_HR_IF_NULL(E_POINTER, Image); - RETURN_HR_IF(E_NOTIMPL, RegistryAuthenticationInformation != nullptr && *RegistryAuthenticationInformation != '\0'); - - auto lock = m_lock.lock_shared(); - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); - - auto [repo, tagOrDigest] = wslutil::ParseImage(Image); - - if (!tagOrDigest.has_value()) - { - tagOrDigest = "latest"; - } - - auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest); - auto io = CreateIOContext(); struct Response @@ -484,19 +467,22 @@ try bool isJson = false; }; - std::optional pullResponse; + std::optional httpResponse; auto onHttpResponse = [&](const boost::beast::http::message& response) { - WSL_LOG("PullHttpResponse", TraceLoggingValue(static_cast(response.result()), "StatusCode")); + WSL_LOG( + "ImageOperationHttpResponse", + TraceLoggingValue(OperationName, "Operation"), + TraceLoggingValue(static_cast(response.result()), "StatusCode")); auto it = response.find(boost::beast::http::field::content_type); - pullResponse.emplace(response.result(), it != response.end() && it->value().starts_with("application/json")); + httpResponse.emplace(response.result(), it != response.end() && it->value().starts_with("application/json")); }; std::string errorJson; std::optional reportedError; auto onChunk = [&](const gsl::span& Content) { - if (pullResponse.has_value() && pullResponse->result != boost::beast::http::status::ok) + if (httpResponse.has_value() && httpResponse->result != boost::beast::http::status::ok) { // If the status code is an error, then this is an error message, not a progress update. errorJson.append(Content.data(), Content.size()); @@ -504,7 +490,11 @@ try } std::string contentString{Content.begin(), Content.end()}; - WSL_LOG("ImagePullProgress", TraceLoggingValue(Image, "Image"), TraceLoggingValue(contentString.c_str(), "Content")); + WSL_LOG( + "ImageOperationProgress", + TraceLoggingValue(OperationName, "Operation"), + TraceLoggingValue(Image, "Image"), + TraceLoggingValue(contentString.c_str(), "Content")); auto parsed = wsl::shared::FromJson(contentString.c_str()); @@ -514,7 +504,8 @@ try { LOG_HR_MSG( E_UNEXPECTED, - "Received multiple error messages during image pull. Previous: %hs, New: %hs", + "Received multiple error messages during image %hs. Previous: %hs, New: %hs", + OperationName, reportedError->c_str(), parsed.errorDetail->message.c_str()); } @@ -533,18 +524,18 @@ try auto onCompleted = [&]() { io.Cancel(); }; io.AddHandle(std::make_unique( - *requestContext, std::move(onHttpResponse), std::move(onChunk), std::move(onCompleted))); + requestContext, std::move(onHttpResponse), std::move(onChunk), std::move(onCompleted))); io.Run({}); - THROW_HR_IF(E_UNEXPECTED, !pullResponse.has_value()); + THROW_HR_IF(E_UNEXPECTED, !httpResponse.has_value()); - if (pullResponse->result != boost::beast::http::status::ok) + if (httpResponse->result != boost::beast::http::status::ok) { std::string errorMessage; - if (pullResponse->isJson) + if (httpResponse->isJson) { - // pull failed, parse the error message. + // operation failed, parse the error message. errorMessage = wsl::shared::FromJson(errorJson.c_str()).message; } else @@ -553,11 +544,11 @@ try errorMessage = errorJson; } - if (pullResponse->result == boost::beast::http::status::not_found) + if (httpResponse->result == boost::beast::http::status::not_found) { THROW_HR_WITH_USER_ERROR(WSLC_E_IMAGE_NOT_FOUND, errorMessage); } - else if (pullResponse->result == boost::beast::http::status::bad_request) + else if (httpResponse->result == boost::beast::http::status::bad_request) { THROW_HR_WITH_USER_ERROR(E_INVALIDARG, errorMessage); } @@ -571,6 +562,34 @@ try // Can happen if an error is returned during progress after receiving an OK status. THROW_HR_WITH_USER_ERROR(E_FAIL, reportedError.value().c_str()); } +} + +HRESULT WSLCSession::PullImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback) +try +{ + COMServiceExecutionContext context; + + RETURN_HR_IF_NULL(E_POINTER, Image); + + auto lock = m_lock.lock_shared(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + + auto [repo, tagOrDigest] = wslutil::ParseImage(Image); + + if (!tagOrDigest.has_value()) + { + tagOrDigest = "latest"; + } + + std::optional registryAuth; + + if (RegistryAuthenticationInformation != nullptr && *RegistryAuthenticationInformation != '\0') + { + registryAuth = std::string(RegistryAuthenticationInformation); + } + + auto requestContext = m_dockerClient->PullImage(repo, tagOrDigest, registryAuth); + StreamImageOperation(*requestContext, Image, "Pull", ProgressCallback); return S_OK; } @@ -1268,6 +1287,32 @@ try } CATCH_RETURN(); +HRESULT WSLCSession::PushImage(LPCSTR Image, LPCSTR RegistryAuthenticationInformation, IProgressCallback* ProgressCallback) +try +{ + COMServiceExecutionContext context; + + RETURN_HR_IF_NULL(E_POINTER, Image); + + auto lock = m_lock.lock_shared(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + + auto [repo, tagOrDigest] = wslutil::ParseImage(Image); + + std::optional registryAuth; + + if (RegistryAuthenticationInformation != nullptr && *RegistryAuthenticationInformation != '\0') + { + registryAuth = std::string(RegistryAuthenticationInformation); + } + + auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, registryAuth); + StreamImageOperation(*requestContext, Image, "Push", ProgressCallback); + + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::InspectImage(_In_ LPCSTR ImageNameOrId, _Out_ LPSTR* Output) try { @@ -1311,6 +1356,35 @@ try } CATCH_RETURN(); +HRESULT WSLCSession::Authenticate(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken) +try +{ + COMServiceExecutionContext context; + + RETURN_HR_IF_NULL(E_POINTER, ServerAddress); + RETURN_HR_IF_NULL(E_POINTER, Username); + RETURN_HR_IF_NULL(E_POINTER, Password); + RETURN_HR_IF_NULL(E_POINTER, IdentityToken); + + *IdentityToken = nullptr; + + auto lock = m_lock.lock_shared(); + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); + + wil::unique_cotaskmem_ansistring token; + + try + { + auto response = m_dockerClient->Authenticate(ServerAddress, Username, Password); + token = wil::make_unique_ansistring(response.c_str()); + } + CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to authenticate with registry: %hs", ServerAddress); + + *IdentityToken = token.release(); + return S_OK; +} +CATCH_RETURN(); + HRESULT WSLCSession::CreateContainer(const WSLCContainerOptions* containerOptions, IWSLCContainer** Container) try { diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index 42a8f82fb..c554dbb6c 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -81,7 +81,9 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession IFACEMETHOD(ListImages)(_In_opt_ const WSLCListImageOptions* Options, _Out_ WSLCImageInformation** Images, _Out_ ULONG* Count) override; IFACEMETHOD(DeleteImage)(_In_ const WSLCDeleteImageOptions* Options, _Out_ WSLCDeletedImageInformation** DeletedImages, _Out_ ULONG* Count) override; IFACEMETHOD(TagImage)(_In_ const WSLCTagImageOptions* Options) override; + IFACEMETHOD(PushImage)(_In_ LPCSTR Image, _In_opt_ LPCSTR RegistryAuthenticationInformation, _In_opt_ IProgressCallback* ProgressCallback) override; IFACEMETHOD(InspectImage)(_In_ LPCSTR ImageNameOrId, _Out_ LPSTR* Output) override; + IFACEMETHOD(Authenticate)(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken) override; // Container management. IFACEMETHOD(CreateContainer)(_In_ const WSLCContainerOptions* Options, _Out_ IWSLCContainer** Container) override; @@ -133,6 +135,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession void RecoverExistingVolumes(); void SaveImageImpl(std::pair& RequestCodePair, WSLCHandle OutputHandle, HANDLE CancelEvent); + void StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback); std::optional m_dockerClient; std::optional m_virtualMachine; diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index e2f8b2caf..7af8e465f 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -36,7 +36,8 @@ target_link_libraries(wsltests VirtDisk.lib Wer.lib Dbghelp.lib - sfc.lib) + sfc.lib + Crypt32.lib) add_dependencies(wsltests wslserviceidl wslclib wslc wslcsdk) add_subdirectory(testplugin) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 42ecb0829..00e4f440e 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -17,6 +17,8 @@ Module Name: #include "wslc.h" #include "WSLCProcessLauncher.h" #include "WSLCContainerLauncher.h" +#include "WSLCLocalRegistry.h" +#include "WslcCredentialStore.h" #include "WslCoreFilesystem.h" using namespace std::literals::chrono_literals; @@ -510,6 +512,52 @@ class WSLCTests } } + TEST_METHOD(AuthenticateTests) + { + WSL2_TEST_ONLY(); + + constexpr auto c_username = "wslctest"; + constexpr auto c_password = "password"; + + auto registryFolder = std::filesystem::current_path() / "registry-storage"; + std::filesystem::create_directories(registryFolder); + + auto registry = wsl::windows::common::WSLCLocalRegistry::Start( + *m_defaultSession, registryFolder, c_username, c_password); + + auto registryUrl = std::format(L"http://{}/v2/", registry.GetServerAddress()); + auto registryAddress = registry.GetServerAddress(); + + // The registry may take to some time before its up and running. Retry until its ready to accept connections. + ExpectHttpResponse(registryUrl.c_str(), 401, true); + + wil::unique_cotaskmem_ansistring token; + VERIFY_SUCCEEDED(m_defaultSession->Authenticate( + registryAddress, + c_username, + c_password, + &token)); + + VERIFY_ARE_EQUAL(E_FAIL, m_defaultSession->Authenticate( + registryAddress, + c_username, + "wrong-password", + &token)); + + ValidateCOMErrorMessage( + std::format(L"login attempt to {} failed with status: 401 Unauthorized", registryUrl).c_str()); + + auto image = "hello-world"; + auto version = "latest"; + auto privateImage = std::format("{}/library/{}:{}", registryAddress, image, version); + + VERIFY_ARE_EQUAL(m_defaultSession->PullImage(privateImage.c_str(), nullptr, nullptr), E_FAIL); + ValidateCOMErrorMessage(std::format(L"Head \"http://{}/v2/{}/manifests/{}\": no basic auth credentials", registryAddress, image, version).c_str()); + + auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); + VERIFY_SUCCEEDED(m_defaultSession->PullImage(privateImage.c_str(), xRegistryAuth.c_str(), nullptr)); + } + TEST_METHOD(ListImages) { WSL2_TEST_ONLY(); From 638ebfa7e9b79b20e71e43b02471011a1a1dabd4 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 11:31:34 -0700 Subject: [PATCH 02/76] Fix test --- test/windows/WSLCTests.cpp | 45 +++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 00e4f440e..9d80afe34 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -532,30 +532,26 @@ class WSLCTests ExpectHttpResponse(registryUrl.c_str(), 401, true); wil::unique_cotaskmem_ansistring token; - VERIFY_SUCCEEDED(m_defaultSession->Authenticate( - registryAddress, - c_username, - c_password, - &token)); - VERIFY_ARE_EQUAL(E_FAIL, m_defaultSession->Authenticate( registryAddress, c_username, "wrong-password", &token)); - - ValidateCOMErrorMessage( - std::format(L"login attempt to {} failed with status: 401 Unauthorized", registryUrl).c_str()); + ValidateCOMErrorMessageContains(L"failed with status: 401 Unauthorized"); - auto image = "hello-world"; - auto version = "latest"; - auto privateImage = std::format("{}/library/{}:{}", registryAddress, image, version); + VERIFY_SUCCEEDED(m_defaultSession->Authenticate( + registryAddress, + c_username, + c_password, + &token)); + VERIFY_IS_NOT_NULL(token.get()); - VERIFY_ARE_EQUAL(m_defaultSession->PullImage(privateImage.c_str(), nullptr, nullptr), E_FAIL); - ValidateCOMErrorMessage(std::format(L"Head \"http://{}/v2/{}/manifests/{}\": no basic auth credentials", registryAddress, image, version).c_str()); + auto image = std::format("{}/library/hello-world:latest", registryAddress); + VERIFY_ARE_EQUAL(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr), E_FAIL); + ValidateCOMErrorMessageContains(L"no basic auth credentials"); auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); - VERIFY_SUCCEEDED(m_defaultSession->PullImage(privateImage.c_str(), xRegistryAuth.c_str(), nullptr)); + VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), xRegistryAuth.c_str(), nullptr)); } TEST_METHOD(ListImages) @@ -1031,6 +1027,25 @@ class WSLCTests } } + void ValidateCOMErrorMessageContains(const std::wstring& ExpectedSubstring) + { + auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo(); + + if (comError.has_value()) + { + if (wcsstr(comError->Message.get(), ExpectedSubstring.c_str()) == nullptr) + { + LogError("Expected COM error containing: '%ls', but got: '%ls'", ExpectedSubstring.c_str(), comError->Message.get()); + VERIFY_FAIL(); + } + } + else + { + LogError("Expected COM error containing: '%ls' but none was set", ExpectedSubstring.c_str()); + VERIFY_FAIL(); + } + } + HRESULT BuildImageFromContext(const std::filesystem::path& contextDir, const WSLCBuildImageOptions* options) { auto dockerfileHandle = wil::open_file((contextDir / "Dockerfile").c_str()); From 7fd402610a4f22a948903cbf64c59b46d8b14751 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 11:37:01 -0700 Subject: [PATCH 03/76] delete cleanup registry storage --- test/windows/WSLCTests.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 9d80afe34..b51aadd95 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -521,6 +521,9 @@ class WSLCTests auto registryFolder = std::filesystem::current_path() / "registry-storage"; std::filesystem::create_directories(registryFolder); + wil:on_scope_exit([&]() { + std::filesystem::remove_all(registryFolder); + }); auto registry = wsl::windows::common::WSLCLocalRegistry::Start( *m_defaultSession, registryFolder, c_username, c_password); From 1fdb1b2fb0f45ab2790fd82d199690c64d6544cc Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 14:52:37 -0700 Subject: [PATCH 04/76] Do not use volume --- src/windows/common/WSLCLocalRegistry.cpp | 52 ++++++++++------------ src/windows/common/WSLCLocalRegistry.h | 1 - src/windows/common/WslcCredentialStore.cpp | 26 +++++++++++ src/windows/common/WslcCredentialStore.h | 6 +++ src/windows/inc/docker_schema.h | 2 +- src/windows/wslcsession/WSLCVhdVolume.cpp | 7 ++- test/windows/WSLCTests.cpp | 48 ++++++++++++++++---- 7 files changed, 101 insertions(+), 41 deletions(-) diff --git a/src/windows/common/WSLCLocalRegistry.cpp b/src/windows/common/WSLCLocalRegistry.cpp index 2340bc4f2..b89cdb2a3 100644 --- a/src/windows/common/WSLCLocalRegistry.cpp +++ b/src/windows/common/WSLCLocalRegistry.cpp @@ -22,37 +22,41 @@ namespace { constexpr auto c_registryImage = "registry:3"; constexpr auto c_htpasswdImage = "httpd:2"; -void PopulateHtpasswd(IWSLCSession& session, const std::filesystem::path& storagePath, const std::string& username, const std::string& password) +std::string GenerateHtpasswd(IWSLCSession& session, const std::string& username, const std::string& password) { - LOG_IF_FAILED(session.PullImage(c_htpasswdImage, nullptr, nullptr)); + THROW_IF_FAILED(session.PullImage(c_htpasswdImage, nullptr, nullptr)); - // Write the htpasswd file into /data/ on the shared folder. - const auto command = std::format("htpasswd -Bbn '{}' '{}' > /data/htpasswd", username, password); + const auto command = std::format("htpasswd -Bbn '{}' '{}'", username, password); - WSLCContainerLauncher launcher("httpd:2", {}, {"/bin/sh", "-c", command}); - launcher.AddVolume(storagePath.wstring(), "/data", false); + WSLCContainerLauncher launcher(c_htpasswdImage, {}, {"/bin/sh", "-c", command}); launcher.SetContainerFlags(WSLCContainerFlagsRm); auto container = launcher.Launch(session); auto result = container.GetInitProcess().WaitAndCaptureOutput(); THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str()); + + auto output = result.Output[1]; + output.erase(output.find_last_not_of("\n\r") + 1); + + THROW_HR_IF_MSG(E_FAIL, output.empty(), "%hs", launcher.FormatResult(result).c_str()); + return output; } -std::vector BuildRegistryEnv(bool withAuth) +std::vector BuildRegistryEnv(IWSLCSession& session, const std::string& username, const std::string& password) { - // Builds the environment variable list for the registry:3 container. std::vector env = { "REGISTRY_HTTP_ADDR=0.0.0.0:5000", - "REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/data/registry", - "REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io" }; - if (withAuth) + if (!username.empty()) { + auto htpasswdEntry = GenerateHtpasswd(session, username, password); + + env.push_back(std::format("HTPASSWD_CONTENT={}", htpasswdEntry)); env.push_back("REGISTRY_AUTH=htpasswd"); env.push_back("REGISTRY_AUTH_HTPASSWD_REALM=WSLC Test Registry"); - env.push_back("REGISTRY_AUTH_HTPASSWD_PATH=/data/htpasswd"); + env.push_back("REGISTRY_AUTH_HTPASSWD_PATH=/htpasswd"); } return env; @@ -79,27 +83,19 @@ WSLCLocalRegistry::~WSLCLocalRegistry() } WSLCLocalRegistry WSLCLocalRegistry::Start( - IWSLCSession& session, const std::filesystem::path& storagePath, const std::string& username, const std::string& password) + IWSLCSession& session, const std::string& username, const std::string& password) { - // Ensure required images are available. - LOG_IF_FAILED(session.PullImage(c_registryImage, nullptr, nullptr)); + THROW_IF_FAILED(session.PullImage(c_registryImage, nullptr, nullptr)); - // If credentials are provided, populate the htpasswd file on the folder. - if (!username.empty()) - { - PopulateHtpasswd(session, storagePath, username, password); - } - - // Launch the registry container. - WSLCContainerLauncher launcher( - c_registryImage, - {}, - {}, - BuildRegistryEnv(!username.empty())); + auto env = BuildRegistryEnv(session, username, password); + WSLCContainerLauncher launcher(c_registryImage, {}, {}, env); launcher.AddPort(5000, 5000, AF_INET); - launcher.AddVolume(storagePath.wstring(), "/data", false); + if (!username.empty()) + { + launcher.SetEntrypoint({"/bin/sh", "-c", "echo \"$HTPASSWD_CONTENT\" > /htpasswd && registry serve /etc/distribution/config.yml"}); + } auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); return WSLCLocalRegistry( diff --git a/src/windows/common/WSLCLocalRegistry.h b/src/windows/common/WSLCLocalRegistry.h index d61c4d717..7af6807ae 100644 --- a/src/windows/common/WSLCLocalRegistry.h +++ b/src/windows/common/WSLCLocalRegistry.h @@ -29,7 +29,6 @@ class WSLCLocalRegistry static WSLCLocalRegistry Start( IWSLCSession& Session, - const std::filesystem::path& StoragePath, const std::string& Username = {}, const std::string& Password = {}); diff --git a/src/windows/common/WslcCredentialStore.cpp b/src/windows/common/WslcCredentialStore.cpp index 3c5d35f70..33789d502 100644 --- a/src/windows/common/WslcCredentialStore.cpp +++ b/src/windows/common/WslcCredentialStore.cpp @@ -41,3 +41,29 @@ std::string wsl::windows::common::BuildRegistryAuthHeader( result.resize(base64Size); return result; } + +std::string wsl::windows::common::BuildRegistryAuthHeader( + const std::string& identityToken, const std::string& serverAddress) +{ + auto authJson = std::format( + R"({{"identitytoken":"{}","serveraddress":"{}"}})", identityToken, serverAddress); + + DWORD base64Size = 0; + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( + reinterpret_cast(authJson.c_str()), + static_cast(authJson.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, + nullptr, + &base64Size)); + + std::string result(base64Size, '\0'); + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( + reinterpret_cast(authJson.c_str()), + static_cast(authJson.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, + result.data(), + &base64Size)); + + result.resize(base64Size); + return result; +} diff --git a/src/windows/common/WslcCredentialStore.h b/src/windows/common/WslcCredentialStore.h index 59b876577..987a210cc 100644 --- a/src/windows/common/WslcCredentialStore.h +++ b/src/windows/common/WslcCredentialStore.h @@ -21,4 +21,10 @@ namespace wsl::windows::common { // (PullImage, PushImage, etc.) from the given credentials. std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress); +// Builds the base64-encoded X-Registry-Auth header value from an identity token +// returned by Authenticate(). +std::string BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress); + +// TODO: Implement credential storage using WinCred + } // namespace wsl::windows::common diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index a0d323fea..8e4b9a33b 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -84,7 +84,7 @@ struct Volume std::string Name; std::string Driver; std::string Mountpoint; - std::map Options; + std::optional> Options; std::map Labels; NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Volume, Name, Driver, Mountpoint, Options, Labels); diff --git a/src/windows/wslcsession/WSLCVhdVolume.cpp b/src/windows/wslcsession/WSLCVhdVolume.cpp index d8430b070..4a1f6f2f1 100644 --- a/src/windows/wslcsession/WSLCVhdVolume.cpp +++ b/src/windows/wslcsession/WSLCVhdVolume.cpp @@ -140,8 +140,11 @@ std::unique_ptr WSLCVhdVolumeImpl::Open( THROW_HR_IF(E_INVALIDARG, vhdMetadata.HostPath.empty()); THROW_HR_IF(E_INVALIDARG, vhdMetadata.SizeBytes == 0); - auto deviceIt = Volume.Options.find("device"); - THROW_HR_IF(E_INVALIDARG, deviceIt == Volume.Options.end()); + WI_VERIFY(Volume.Options.has_value()); + auto options = Volume.Options.value(); + + auto deviceIt = options.find("device"); + THROW_HR_IF(E_INVALIDARG, deviceIt == options.end()); THROW_HR_IF(E_INVALIDARG, deviceIt->second.empty()); std::string virtualMachinePath = deviceIt->second; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index b51aadd95..0de323e15 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -87,6 +87,11 @@ class WSLCTests LoadTestImage("python:3.12-alpine"); } + if (!hasImage("hello-world:latest")) + { + LoadTestImage("hello-world:latest"); + } + PruneResult result; VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result)); if (result.result.ContainersCount > 0) @@ -169,6 +174,32 @@ class WSLCTests return RunningWSLCContainer(std::move(rawContainer), {}); } + void PushImageToRegistry( + IWSLCSession& session, const std::string& imageName, const char* registryAddress, const std::string& registryAuth) + { + auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); + auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest")); + auto registryRepo = std::format("{}/{}", registryAddress, repo); + auto registryTag = tag.value_or("latest"); + + WSLCTagImageOptions tagOptions{}; + tagOptions.Image = imageName.c_str(); + tagOptions.Repo = registryRepo.c_str(); + tagOptions.Tag = registryTag.c_str(); + + // Tag the image with the registry address so it can be pushed. + VERIFY_SUCCEEDED(session.TagImage(&tagOptions)); + + // Ensures the tag is removed to allow tests to try to push or pull the same image again. + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + WSLCDeleteImageOptions deleteOptions{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsNone}; + wil::unique_cotaskmem_array_ptr deletedImages; + LOG_IF_FAILED(session.DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address())); + }); + + VERIFY_SUCCEEDED(session.PushImage(registryImage.c_str(), registryAuth.c_str(), nullptr)); + } + TEST_METHOD(GetVersion) { WSL2_TEST_ONLY(); @@ -519,14 +550,8 @@ class WSLCTests constexpr auto c_username = "wslctest"; constexpr auto c_password = "password"; - auto registryFolder = std::filesystem::current_path() / "registry-storage"; - std::filesystem::create_directories(registryFolder); - wil:on_scope_exit([&]() { - std::filesystem::remove_all(registryFolder); - }); - auto registry = wsl::windows::common::WSLCLocalRegistry::Start( - *m_defaultSession, registryFolder, c_username, c_password); + *m_defaultSession, c_username, c_password); auto registryUrl = std::format(L"http://{}/v2/", registry.GetServerAddress()); auto registryAddress = registry.GetServerAddress(); @@ -549,12 +574,17 @@ class WSLCTests &token)); VERIFY_IS_NOT_NULL(token.get()); - auto image = std::format("{}/library/hello-world:latest", registryAddress); + auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); + PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, xRegistryAuth); + + // Pulling without credentials should fail. + auto image = std::format("{}/hello-world:latest", registryAddress); VERIFY_ARE_EQUAL(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr), E_FAIL); ValidateCOMErrorMessageContains(L"no basic auth credentials"); - auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); + // Pulling with credentials should succeed. VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), xRegistryAuth.c_str(), nullptr)); + ExpectImagePresent(*m_defaultSession, image.c_str()); } TEST_METHOD(ListImages) From 35e63223dfa568bce7c8530021181a0b375f7a95 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 15:07:07 -0700 Subject: [PATCH 05/76] Undo entry point fix (getting checked in seperately) --- src/windows/inc/docker_schema.h | 2 +- src/windows/wslcsession/WSLCContainer.cpp | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index 8e4b9a33b..e7e3fd862 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -165,7 +165,7 @@ struct CreateContainer std::optional StopSignal; std::optional WorkingDir; std::vector Cmd; - std::optional> Entrypoint; + std::vector Entrypoint; std::vector Env; std::map ExposedPorts; std::map Labels; diff --git a/src/windows/wslcsession/WSLCContainer.cpp b/src/windows/wslcsession/WSLCContainer.cpp index 78ff2c46b..35d3000a0 100644 --- a/src/windows/wslcsession/WSLCContainer.cpp +++ b/src/windows/wslcsession/WSLCContainer.cpp @@ -1037,16 +1037,8 @@ std::unique_ptr WSLCContainerImpl::Create( request.OpenStdin = true; } - if (containerOptions.InitProcessOptions.CommandLine.Count > 0) - { - request.Cmd = StringArrayToVector(containerOptions.InitProcessOptions.CommandLine); - } - - if (containerOptions.Entrypoint.Count > 0) - { - request.Entrypoint = StringArrayToVector(containerOptions.Entrypoint); - } - + request.Cmd = StringArrayToVector(containerOptions.InitProcessOptions.CommandLine); + request.Entrypoint = StringArrayToVector(containerOptions.Entrypoint); request.Env = StringArrayToVector(containerOptions.InitProcessOptions.Environment); if (containerOptions.StopSignal != WSLCSignalNone) From b278c0a38d1780b8f3a8bb470fae469a9c6a59f7 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 15:09:07 -0700 Subject: [PATCH 06/76] undo entry point fix --- src/windows/inc/docker_schema.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/inc/docker_schema.h b/src/windows/inc/docker_schema.h index e7e3fd862..92542a13c 100644 --- a/src/windows/inc/docker_schema.h +++ b/src/windows/inc/docker_schema.h @@ -165,7 +165,7 @@ struct CreateContainer std::optional StopSignal; std::optional WorkingDir; std::vector Cmd; - std::vector Entrypoint; + std::vector Entrypoint; // TODO: Find a way to omit if the caller wants the default entrypoint. std::vector Env; std::map ExposedPorts; std::map Labels; From b98878358c26a60b043d108cf629e5cd999f28f8 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 15:10:05 -0700 Subject: [PATCH 07/76] Fix formatting --- src/windows/wslcsession/DockerHTTPClient.cpp | 14 ++++++++++---- src/windows/wslcsession/DockerHTTPClient.h | 12 +++++++++--- src/windows/wslcsession/WSLCSession.cpp | 3 +-- test/windows/WSLCTests.cpp | 20 +++++--------------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index f61562c2d..07ec471b2 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -118,7 +118,8 @@ DockerHTTPClient::DockerHTTPClient(wsl::shared::SocketChannel&& Channel, HANDLE { } -std::unique_ptr DockerHTTPClient::PullImage(const std::string& Repo, const std::optional& tagOrDigest, const std::optional& registryAuth) +std::unique_ptr DockerHTTPClient::PullImage( + const std::string& Repo, const std::optional& tagOrDigest, const std::optional& registryAuth) { auto url = URL::Create("/images/create"); @@ -132,7 +133,7 @@ std::unique_ptr DockerHTTPClient::PullImag } std::map customHeaders; - + if (registryAuth.has_value()) { customHeaders["X-Registry-Auth"] = registryAuth.value(); @@ -170,7 +171,8 @@ void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, Transaction(verb::post, url); } -std::unique_ptr DockerHTTPClient::PushImage(const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth) +std::unique_ptr DockerHTTPClient::PushImage( + const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth) { auto url = URL::Create("/images/{}/push", ImageName); @@ -666,7 +668,11 @@ void DockerHTTPClient::DockerHttpResponseHandle::OnResponseBytes(const gsl::span } std::unique_ptr DockerHTTPClient::SendRequestImpl( - verb Method, const URL& Url, const std::string& Body, const std::map& Headers, const std::map& CustomHeaders) + verb Method, + const URL& Url, + const std::string& Body, + const std::map& Headers, + const std::map& CustomHeaders) { auto context = std::make_unique(ConnectSocket()); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index e577bb675..ee03757f5 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -151,11 +151,13 @@ class DockerHTTPClient std::vector labels; }; - std::unique_ptr PullImage(const std::string& Repo, const std::optional& tagOrDigest, const std::optional& registryAuth = std::nullopt); + std::unique_ptr PullImage( + const std::string& Repo, const std::optional& tagOrDigest, const std::optional& registryAuth = std::nullopt); std::unique_ptr ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength); std::unique_ptr LoadImage(uint64_t ContentLength); void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag); - std::unique_ptr PushImage(const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth = std::nullopt); + std::unique_ptr PushImage( + const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth = std::nullopt); std::string Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password); std::vector ListImages(bool all = false, bool digests = false, const ListImagesFilters& filters = {}); common::docker_schema::InspectImage InspectImage(const std::string& NameOrId); @@ -226,7 +228,11 @@ class DockerHTTPClient wil::unique_socket ConnectSocket(); std::unique_ptr SendRequestImpl( - boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map& Headers = {}, const std::map& CustomHeaders = {}); + boost::beast::http::verb Method, + const URL& Url, + const std::string& Body, + const std::map& Headers = {}, + const std::map& CustomHeaders = {}); std::pair SendRequestAndReadResponse( boost::beast::http::verb Method, const URL& Url, const std::string& Body = ""); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index b382f0ed4..20470d453 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -461,8 +461,7 @@ void WSLCSession::StartDockerd() m_dockerdProcess->GetExitEvent(), std::bind(&WSLCSession::OnDockerdExited, this))); } -void WSLCSession::StreamImageOperation( - DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback) +void WSLCSession::StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback) { auto io = CreateIOContext(); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index fc5f75ce8..27c957826 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -174,8 +174,7 @@ class WSLCTests return RunningWSLCContainer(std::move(rawContainer), {}); } - void PushImageToRegistry( - IWSLCSession& session, const std::string& imageName, const char* registryAddress, const std::string& registryAuth) + void PushImageToRegistry(IWSLCSession& session, const std::string& imageName, const char* registryAddress, const std::string& registryAuth) { auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest")); @@ -581,28 +580,19 @@ class WSLCTests constexpr auto c_username = "wslctest"; constexpr auto c_password = "password"; - auto registry = wsl::windows::common::WSLCLocalRegistry::Start( - *m_defaultSession, c_username, c_password); + auto registry = wsl::windows::common::WSLCLocalRegistry::Start(*m_defaultSession, c_username, c_password); auto registryUrl = std::format(L"http://{}/v2/", registry.GetServerAddress()); auto registryAddress = registry.GetServerAddress(); - + // The registry may take to some time before its up and running. Retry until its ready to accept connections. ExpectHttpResponse(registryUrl.c_str(), 401, true); wil::unique_cotaskmem_ansistring token; - VERIFY_ARE_EQUAL(E_FAIL, m_defaultSession->Authenticate( - registryAddress, - c_username, - "wrong-password", - &token)); + VERIFY_ARE_EQUAL(E_FAIL, m_defaultSession->Authenticate(registryAddress, c_username, "wrong-password", &token)); ValidateCOMErrorMessageContains(L"failed with status: 401 Unauthorized"); - VERIFY_SUCCEEDED(m_defaultSession->Authenticate( - registryAddress, - c_username, - c_password, - &token)); + VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress, c_username, c_password, &token)); VERIFY_IS_NOT_NULL(token.get()); auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); From 783ae7b4737d7b8d24408551d3e0a4f2b8000033 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 15:18:06 -0700 Subject: [PATCH 08/76] Address copilot comment --- src/windows/common/WslcCredentialStore.cpp | 62 ++++++++++------------ 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/src/windows/common/WslcCredentialStore.cpp b/src/windows/common/WslcCredentialStore.cpp index 33789d502..149d5da15 100644 --- a/src/windows/common/WslcCredentialStore.cpp +++ b/src/windows/common/WslcCredentialStore.cpp @@ -13,57 +13,53 @@ Module Name: --*/ #include "WslcCredentialStore.h" -#include +#include #include -std::string wsl::windows::common::BuildRegistryAuthHeader( - const std::string& username, const std::string& password, const std::string& serverAddress) -{ - auto authJson = std::format( - R"({{"username":"{}","password":"{}","serveraddress":"{}"}})", username, password, serverAddress); +namespace { +std::string Base64Encode(const std::string& input) +{ DWORD base64Size = 0; THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( - reinterpret_cast(authJson.c_str()), - static_cast(authJson.size()), + reinterpret_cast(input.c_str()), + static_cast(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &base64Size)); - std::string result(base64Size, '\0'); + auto buffer = std::make_unique(base64Size); THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( - reinterpret_cast(authJson.c_str()), - static_cast(authJson.size()), + reinterpret_cast(input.c_str()), + static_cast(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, - result.data(), + buffer.get(), &base64Size)); - result.resize(base64Size); - return result; + return std::string(buffer.get()); } +} // namespace + std::string wsl::windows::common::BuildRegistryAuthHeader( - const std::string& identityToken, const std::string& serverAddress) + const std::string& username, const std::string& password, const std::string& serverAddress) { - auto authJson = std::format( - R"({{"identitytoken":"{}","serveraddress":"{}"}})", identityToken, serverAddress); + nlohmann::json authJson = { + {"username", username}, + {"password", password}, + {"serveraddress", serverAddress} + }; - DWORD base64Size = 0; - THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( - reinterpret_cast(authJson.c_str()), - static_cast(authJson.size()), - CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, - nullptr, - &base64Size)); + return Base64Encode(authJson.dump()); +} - std::string result(base64Size, '\0'); - THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( - reinterpret_cast(authJson.c_str()), - static_cast(authJson.size()), - CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, - result.data(), - &base64Size)); +std::string wsl::windows::common::BuildRegistryAuthHeader( + const std::string& identityToken, const std::string& serverAddress) +{ + nlohmann::json authJson = { + {"identitytoken", identityToken}, + {"serveraddress", serverAddress} + }; - result.resize(base64Size); - return result; + return Base64Encode(authJson.dump()); } From ead90fcbdcb6e895ae1db512ae49df8bcae0d7c9 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 16:50:40 -0700 Subject: [PATCH 09/76] Use packaged wslc-registry --- packages.config | 2 +- src/windows/common/WSLCLocalRegistry.cpp | 43 ++++-------------------- test/windows/Common.cpp | 4 +++ test/windows/WSLCTests.cpp | 5 +++ 4 files changed, 16 insertions(+), 38 deletions(-) diff --git a/packages.config b/packages.config index 3063f85a1..98e332420 100644 --- a/packages.config +++ b/packages.config @@ -21,7 +21,7 @@ - + diff --git a/src/windows/common/WSLCLocalRegistry.cpp b/src/windows/common/WSLCLocalRegistry.cpp index b89cdb2a3..c4449cd70 100644 --- a/src/windows/common/WSLCLocalRegistry.cpp +++ b/src/windows/common/WSLCLocalRegistry.cpp @@ -19,31 +19,9 @@ using wsl::windows::common::WSLCLocalRegistry; namespace { -constexpr auto c_registryImage = "registry:3"; -constexpr auto c_htpasswdImage = "httpd:2"; +constexpr auto c_registryImage = "wslc-registry:latest"; -std::string GenerateHtpasswd(IWSLCSession& session, const std::string& username, const std::string& password) -{ - THROW_IF_FAILED(session.PullImage(c_htpasswdImage, nullptr, nullptr)); - - const auto command = std::format("htpasswd -Bbn '{}' '{}'", username, password); - - WSLCContainerLauncher launcher(c_htpasswdImage, {}, {"/bin/sh", "-c", command}); - launcher.SetContainerFlags(WSLCContainerFlagsRm); - - auto container = launcher.Launch(session); - auto result = container.GetInitProcess().WaitAndCaptureOutput(); - - THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str()); - - auto output = result.Output[1]; - output.erase(output.find_last_not_of("\n\r") + 1); - - THROW_HR_IF_MSG(E_FAIL, output.empty(), "%hs", launcher.FormatResult(result).c_str()); - return output; -} - -std::vector BuildRegistryEnv(IWSLCSession& session, const std::string& username, const std::string& password) +std::vector BuildRegistryEnv(const std::string& username, const std::string& password) { std::vector env = { "REGISTRY_HTTP_ADDR=0.0.0.0:5000", @@ -51,12 +29,8 @@ std::vector BuildRegistryEnv(IWSLCSession& session, const std::stri if (!username.empty()) { - auto htpasswdEntry = GenerateHtpasswd(session, username, password); - - env.push_back(std::format("HTPASSWD_CONTENT={}", htpasswdEntry)); - env.push_back("REGISTRY_AUTH=htpasswd"); - env.push_back("REGISTRY_AUTH_HTPASSWD_REALM=WSLC Test Registry"); - env.push_back("REGISTRY_AUTH_HTPASSWD_PATH=/htpasswd"); + env.push_back(std::format("USERNAME={}", username)); + env.push_back(std::format("PASSWORD={}", password)); } return env; @@ -85,17 +59,12 @@ WSLCLocalRegistry::~WSLCLocalRegistry() WSLCLocalRegistry WSLCLocalRegistry::Start( IWSLCSession& session, const std::string& username, const std::string& password) { - THROW_IF_FAILED(session.PullImage(c_registryImage, nullptr, nullptr)); - - auto env = BuildRegistryEnv(session, username, password); + auto env = BuildRegistryEnv(username, password); WSLCContainerLauncher launcher(c_registryImage, {}, {}, env); + launcher.SetEntrypoint({"/entrypoint.sh"}); launcher.AddPort(5000, 5000, AF_INET); - if (!username.empty()) - { - launcher.SetEntrypoint({"/bin/sh", "-c", "echo \"$HTPASSWD_CONTENT\" > /htpasswd && registry serve /etc/distribution/config.yml"}); - } auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); return WSLCLocalRegistry( diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index 67b0a5056..7a163f76d 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -2843,6 +2843,10 @@ std::filesystem::path GetTestImagePath(std::string_view imageName) { result /= L"HelloWorldSaved.tar"; } + else if (imageName == "wslc-registry:latest") + { + result /= L"wslc-registry.tar"; + } else { THROW_HR_MSG(E_INVALIDARG, "Unknown test image: %hs", imageName.data()); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 27c957826..0859dbd8a 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -92,6 +92,11 @@ class WSLCTests LoadTestImage("hello-world:latest"); } + if (!hasImage("wslc-registry:latest")) + { + LoadTestImage("wslc-registry:latest"); + } + PruneResult result; VERIFY_SUCCEEDED(m_defaultSession->PruneContainers(nullptr, 0, 0, &result.result)); if (result.result.ContainersCount > 0) From 5156db571fcec3e5a1158ac1a8c6bbccb92cea7c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 17:08:40 -0700 Subject: [PATCH 10/76] Address copilot comments --- test/windows/WSLCTests.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 0859dbd8a..67d26fec8 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -590,11 +590,11 @@ class WSLCTests auto registryUrl = std::format(L"http://{}/v2/", registry.GetServerAddress()); auto registryAddress = registry.GetServerAddress(); - // The registry may take to some time before its up and running. Retry until its ready to accept connections. + // The registry may take some time before it's up and running. Retry until it's ready to accept connections. ExpectHttpResponse(registryUrl.c_str(), 401, true); wil::unique_cotaskmem_ansistring token; - VERIFY_ARE_EQUAL(E_FAIL, m_defaultSession->Authenticate(registryAddress, c_username, "wrong-password", &token)); + VERIFY_ARE_EQUAL(m_defaultSession->Authenticate(registryAddress, c_username, "wrong-password", &token), E_FAIL); ValidateCOMErrorMessageContains(L"failed with status: 401 Unauthorized"); VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress, c_username, c_password, &token)); @@ -1081,6 +1081,12 @@ class WSLCTests if (comError.has_value()) { + if (!comError->Message) + { + LogError("Expected COM error containing: '%ls', but COM error message was null", ExpectedSubstring.c_str()); + VERIFY_FAIL(); + } + if (wcsstr(comError->Message.get(), ExpectedSubstring.c_str()) == nullptr) { LogError("Expected COM error containing: '%ls', but got: '%ls'", ExpectedSubstring.c_str(), comError->Message.get()); From 31b5ad53b33c67fbc5ff63eedab1a7bf395c1c32 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 17:13:06 -0700 Subject: [PATCH 11/76] Address more copilot comments --- src/windows/common/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index 503bd89ea..99a9efc55 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -138,7 +138,6 @@ set(HEADERS add_library(common STATIC ${SOURCES} ${HEADERS}) add_dependencies(common wslserviceidl wslcidl localization wslservicemc wslinstalleridl) -target_link_libraries(common PRIVATE Crypt32.lib) target_precompile_headers(common PRIVATE precomp.h) set_target_properties(common PROPERTIES FOLDER windows) From 6305c58e691014084a23271d8dec2bfe4c04fcd0 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 18:07:51 -0700 Subject: [PATCH 12/76] Fix push and Pulll tests --- src/windows/wslcsession/DockerHTTPClient.cpp | 10 +- src/windows/wslcsession/DockerHTTPClient.h | 3 +- src/windows/wslcsession/WSLCSession.cpp | 10 +- test/windows/WSLCTests.cpp | 134 +++++++++---------- 4 files changed, 66 insertions(+), 91 deletions(-) diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 07ec471b2..3ac26a5fe 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -172,7 +172,7 @@ void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, } std::unique_ptr DockerHTTPClient::PushImage( - const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth) + const std::string& ImageName, const std::optional& tag, std::string& registryAuth) { auto url = URL::Create("/images/{}/push", ImageName); @@ -181,13 +181,7 @@ std::unique_ptr DockerHTTPClient::PushImag url.SetParameter("tag", tag.value()); } - std::map customHeaders; - - if (registryAuth.has_value()) - { - customHeaders["X-Registry-Auth"] = registryAuth.value(); - } - + std::map customHeaders = {{"X-Registry-Auth", registryAuth}}; return SendRequestImpl(verb::post, url, {}, {}, customHeaders); } diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index ee03757f5..9f448d23d 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -156,8 +156,7 @@ class DockerHTTPClient std::unique_ptr ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength); std::unique_ptr LoadImage(uint64_t ContentLength); void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag); - std::unique_ptr PushImage( - const std::string& ImageName, const std::optional& tag, const std::optional& registryAuth = std::nullopt); + std::unique_ptr PushImage(const std::string& ImageName, const std::optional& tag, std::string& registryAuth); std::string Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password); std::vector ListImages(bool all = false, bool digests = false, const ListImagesFilters& filters = {}); common::docker_schema::InspectImage InspectImage(const std::string& NameOrId); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 20470d453..2f39ed9d8 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -1297,19 +1297,13 @@ try COMServiceExecutionContext context; RETURN_HR_IF_NULL(E_POINTER, Image); + RETURN_HR_IF_NULL(E_POINTER, RegistryAuthenticationInformation); auto lock = m_lock.lock_shared(); THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); auto [repo, tagOrDigest] = wslutil::ParseImage(Image); - - std::optional registryAuth; - - if (RegistryAuthenticationInformation != nullptr && *RegistryAuthenticationInformation != '\0') - { - registryAuth = std::string(RegistryAuthenticationInformation); - } - + std::string registryAuth = RegistryAuthenticationInformation; auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, registryAuth); StreamImageOperation(*requestContext, Image, "Push", ProgressCallback); diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 67d26fec8..f005dde26 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -92,6 +92,11 @@ class WSLCTests LoadTestImage("hello-world:latest"); } + if (!hasImage("alpine:latest")) + { + LoadTestImage("alpine:latest"); + } + if (!hasImage("wslc-registry:latest")) { LoadTestImage("wslc-registry:latest"); @@ -179,7 +184,7 @@ class WSLCTests return RunningWSLCContainer(std::move(rawContainer), {}); } - void PushImageToRegistry(IWSLCSession& session, const std::string& imageName, const char* registryAddress, const std::string& registryAuth) + void PushImageToRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth) { auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest")); @@ -453,27 +458,29 @@ class WSLCTests WSL2_TEST_ONLY(); { - HRESULT pullResult = m_defaultSession->PullImage("hello-world:linux", nullptr, nullptr); + // Start a local registry without auth and push hello-world:latest to it. + auto registry = wsl::windows::common::WSLCLocalRegistry::Start(*m_defaultSession); + auto registryAddress = registry.GetServerAddress(); - // Skip test if error is due to rate limit. - if (pullResult == E_FAIL) - { - auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo(); - if (comError.has_value()) - { - if (wcsstr(comError->Message.get(), L"toomanyrequests") != nullptr) - { - LogWarning("Skipping PullImage test due to rate limiting."); - return; - } - } - } + // Wait for the registry to be ready. + auto registryUrl = std::format(L"http://{}/v2/", registryAddress); + ExpectHttpResponse(registryUrl.c_str(), 200, true); + + auto auth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); + PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, auth); - VERIFY_SUCCEEDED(pullResult); + auto image = std::format("{}/hello-world:latest", registryAddress); + + // Delete the image if it already exists locally, so the pull is a real pull. + WSLCDeleteImageOptions deleteOptions{.Image = image.c_str(), .Flags = WSLCDeleteImageFlagsForce}; + wil::unique_cotaskmem_array_ptr deletedImages; + LOG_IF_FAILED(m_defaultSession->DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address())); + + VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr)); // Verify that the image is in the list of images. - ExpectImagePresent(*m_defaultSession, "hello-world:linux"); - WSLCContainerLauncher launcher("hello-world:linux", "wslc-pull-image-container"); + ExpectImagePresent(*m_defaultSession, image.c_str()); + WSLCContainerLauncher launcher(image, "wslc-pull-image-container"); auto container = launcher.Launch(*m_defaultSession); auto result = container.GetInitProcess().WaitAndCaptureOutput(); @@ -488,10 +495,7 @@ class WSLCTests L"access to the resource is denied"; VERIFY_ARE_EQUAL(m_defaultSession->PullImage("does-not:exist", nullptr, nullptr), WSLC_E_IMAGE_NOT_FOUND); - auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo(); - VERIFY_IS_TRUE(comError.has_value()); - - VERIFY_ARE_EQUAL(expectedError, comError->Message.get()); + ValidateCOMErrorMessage(expectedError.c_str()); } // Validate that PullImage() returns the appropriate error if the session is terminated. @@ -510,71 +514,55 @@ class WSLCTests { WSL2_TEST_ONLY(); - // TODO: Enable once custom registries are supported, to avoid hitting public registry rate limits. - SKIP_TEST_UNSTABLE(); + // Start a local registry without auth to avoid Docker Hub rate limits. + auto registry = wsl::windows::common::WSLCLocalRegistry::Start(*m_defaultSession); + auto registryAddress = registry.GetServerAddress(); - auto validatePull = [&](const std::string& Image, const std::optional& ExpectedTag = {}) { - VERIFY_SUCCEEDED(m_defaultSession->PullImage(Image.c_str(), nullptr, nullptr)); + // Wait for the registry to be ready. + auto registryUrl = std::format(L"http://{}/v2/", registryAddress); + ExpectHttpResponse(registryUrl.c_str(), 200, true); - auto cleanup = wil::scope_exit( - [&]() { LOG_IF_FAILED(DeleteImageNoThrow(ExpectedTag.value_or(Image), WSLCDeleteImageFlagsForce).first); }); + auto validatePull = [&](const std::string& sourceImage, const std::string& registryTag) { + // Push the source image to the local registry. + auto auth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); + PushImageToRegistry(*m_defaultSession, sourceImage, registryAddress, auth); - if (!ExpectedTag.has_value()) - { - wil::unique_cotaskmem_array_ptr images; - VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address())); + auto registryImage = std::format("{}/{}", registryAddress, registryTag); - for (const auto& e : images) - { - wil::unique_cotaskmem_ansistring json; - VERIFY_SUCCEEDED(m_defaultSession->InspectImage(e.Hash, &json)); + // Delete the image locally so the pull is a real pull. + WSLCDeleteImageOptions deleteOptions{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsForce}; + wil::unique_cotaskmem_array_ptr deletedImages; + LOG_IF_FAILED(m_defaultSession->DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address())); - auto parsed = wsl::shared::FromJson(json.get()); + VERIFY_SUCCEEDED(m_defaultSession->PullImage(registryImage.c_str(), nullptr, nullptr)); - for (const auto& repoTag : parsed.RepoDigests.value_or({})) - { - if (Image == repoTag) - { - return; - } - } - } - - LogError("Expected digest '%hs' not found ", Image.c_str()); + auto cleanup = + wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsForce).first); }); - VERIFY_FAIL(); - } - else - { - ExpectImagePresent(*m_defaultSession, ExpectedTag->c_str()); - } + ExpectImagePresent(*m_defaultSession, registryImage.c_str()); }; - validatePull("ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30", {}); - validatePull("ubuntu", "ubuntu:latest"); - validatePull("debian:bookworm", "debian:bookworm"); - validatePull("pytorch/pytorch", "pytorch/pytorch:latest"); - validatePull("registry.k8s.io/pause:3.2", "registry.k8s.io/pause:3.2"); + validatePull("debian:latest", "debian:latest"); + validatePull("alpine:latest", "alpine:latest"); + validatePull("hello-world:latest", "hello-world:latest"); + } + + TEST_METHOD(PushImage) + { + WSL2_TEST_ONLY(); - // Validate that PullImage() fails appropriately when the session runs out of space. + // Validate that pushing a non-existent image fails. { - auto settings = GetDefaultSessionSettings(L"wslc-pull-image-out-of-space", false); - settings.NetworkingMode = WSLCNetworkingModeVirtioProxy; - settings.MemoryMb = 1024; - auto session = CreateSession(settings); + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", nullptr, nullptr), E_INVALIDARG); + } - VERIFY_ARE_EQUAL(session->PullImage("pytorch/pytorch", nullptr, nullptr), E_FAIL); + // Validate that PushImage() returns the appropriate error if the session is terminated. + { + VERIFY_SUCCEEDED(m_defaultSession->Terminate()); - auto comError = wsl::windows::common::wslutil::GetCOMErrorInfo(); - VERIFY_IS_TRUE(comError.has_value()); + auto cleanup = wil::scope_exit([&]() { ResetTestSession(); }); - // The error message can't be compared directly because it contains an unpredicable path: - // "write /var/lib/docker/tmp/GetImageBlob1760660623: no space left on device" - if (StrStrW(comError->Message.get(), L"no space left on device") == nullptr) - { - LogError("Unexpected error message: %ls", comError->Message.get()); - VERIFY_FAIL(); - } + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("hello-world:latest", nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } } From 22cb648bf26a9e863ba95542478ab589c5e3faaa Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 18:09:42 -0700 Subject: [PATCH 13/76] Fix formatting --- src/windows/common/WSLCLocalRegistry.cpp | 15 +++----------- src/windows/common/WSLCLocalRegistry.h | 11 ++--------- src/windows/common/WslcCredentialStore.cpp | 23 +++++----------------- 3 files changed, 10 insertions(+), 39 deletions(-) diff --git a/src/windows/common/WSLCLocalRegistry.cpp b/src/windows/common/WSLCLocalRegistry.cpp index c4449cd70..666d1f3b2 100644 --- a/src/windows/common/WSLCLocalRegistry.cpp +++ b/src/windows/common/WSLCLocalRegistry.cpp @@ -38,11 +38,7 @@ std::vector BuildRegistryEnv(const std::string& username, const std } // namespace -WSLCLocalRegistry::WSLCLocalRegistry( - IWSLCSession& session, - RunningWSLCContainer&& container, - std::string&& username, - std::string&& password) : +WSLCLocalRegistry::WSLCLocalRegistry(IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password) : m_session(wil::com_ptr(&session)), m_username(std::move(username)), m_password(std::move(password)), @@ -56,8 +52,7 @@ WSLCLocalRegistry::~WSLCLocalRegistry() m_container.Reset(); } -WSLCLocalRegistry WSLCLocalRegistry::Start( - IWSLCSession& session, const std::string& username, const std::string& password) +WSLCLocalRegistry WSLCLocalRegistry::Start(IWSLCSession& session, const std::string& username, const std::string& password) { auto env = BuildRegistryEnv(username, password); @@ -67,11 +62,7 @@ WSLCLocalRegistry WSLCLocalRegistry::Start( auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); - return WSLCLocalRegistry( - session, - std::move(container), - std::string(username), - std::string(password)); + return WSLCLocalRegistry(session, std::move(container), std::string(username), std::string(password)); } const char* WSLCLocalRegistry::GetServerAddress() const diff --git a/src/windows/common/WSLCLocalRegistry.h b/src/windows/common/WSLCLocalRegistry.h index 7af6807ae..3c0fd079d 100644 --- a/src/windows/common/WSLCLocalRegistry.h +++ b/src/windows/common/WSLCLocalRegistry.h @@ -27,21 +27,14 @@ class WSLCLocalRegistry DEFAULT_MOVABLE(WSLCLocalRegistry); ~WSLCLocalRegistry(); - static WSLCLocalRegistry Start( - IWSLCSession& Session, - const std::string& Username = {}, - const std::string& Password = {}); + static WSLCLocalRegistry Start(IWSLCSession& Session, const std::string& Username = {}, const std::string& Password = {}); const char* GetServerAddress() const; const std::string& GetUsername() const; const std::string& GetPassword() const; private: - WSLCLocalRegistry( - IWSLCSession& session, - RunningWSLCContainer&& container, - std::string&& username, - std::string&& password); + WSLCLocalRegistry(IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password); wil::com_ptr m_session; std::string m_username; diff --git a/src/windows/common/WslcCredentialStore.cpp b/src/windows/common/WslcCredentialStore.cpp index 149d5da15..a66b6587c 100644 --- a/src/windows/common/WslcCredentialStore.cpp +++ b/src/windows/common/WslcCredentialStore.cpp @@ -22,11 +22,7 @@ std::string Base64Encode(const std::string& input) { DWORD base64Size = 0; THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( - reinterpret_cast(input.c_str()), - static_cast(input.size()), - CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, - nullptr, - &base64Size)); + reinterpret_cast(input.c_str()), static_cast(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &base64Size)); auto buffer = std::make_unique(base64Size); THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( @@ -41,25 +37,16 @@ std::string Base64Encode(const std::string& input) } // namespace -std::string wsl::windows::common::BuildRegistryAuthHeader( - const std::string& username, const std::string& password, const std::string& serverAddress) +std::string wsl::windows::common::BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress) { - nlohmann::json authJson = { - {"username", username}, - {"password", password}, - {"serveraddress", serverAddress} - }; + nlohmann::json authJson = {{"username", username}, {"password", password}, {"serveraddress", serverAddress}}; return Base64Encode(authJson.dump()); } -std::string wsl::windows::common::BuildRegistryAuthHeader( - const std::string& identityToken, const std::string& serverAddress) +std::string wsl::windows::common::BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress) { - nlohmann::json authJson = { - {"identitytoken", identityToken}, - {"serveraddress", serverAddress} - }; + nlohmann::json authJson = {{"identitytoken", identityToken}, {"serveraddress", serverAddress}}; return Base64Encode(authJson.dump()); } From 83b39f226ec51585e80c06cba8391e21534f3ba7 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 18:20:19 -0700 Subject: [PATCH 14/76] Address suggestion --- src/windows/service/inc/wslc.idl | 2 +- src/windows/wslcsession/WSLCSession.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index b55d25618..3d3b4be98 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -591,7 +591,7 @@ interface IWSLCSession : IUnknown HRESULT ListImages([in, unique] const WSLCListImageOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count); HRESULT DeleteImage([in] const WSLCDeleteImageOptions* Options, [out, size_is(, *Count)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* Count); HRESULT TagImage([in] const WSLCTagImageOptions* Options); - HRESULT PushImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); + HRESULT PushImage([in] LPCSTR Image, [in] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); HRESULT InspectImage([in] LPCSTR ImageNameOrId, [out] LPSTR* Output); HRESULT Authenticate([in] LPCSTR ServerAddress, [in] LPCSTR Username, [in] LPCSTR Password, [out] LPSTR* IdentityToken); diff --git a/src/windows/wslcsession/WSLCSession.h b/src/windows/wslcsession/WSLCSession.h index c4dce0078..55b65b9b9 100644 --- a/src/windows/wslcsession/WSLCSession.h +++ b/src/windows/wslcsession/WSLCSession.h @@ -81,7 +81,7 @@ class DECLSPEC_UUID("4877FEFC-4977-4929-A958-9F36AA1892A4") WSLCSession IFACEMETHOD(ListImages)(_In_opt_ const WSLCListImageOptions* Options, _Out_ WSLCImageInformation** Images, _Out_ ULONG* Count) override; IFACEMETHOD(DeleteImage)(_In_ const WSLCDeleteImageOptions* Options, _Out_ WSLCDeletedImageInformation** DeletedImages, _Out_ ULONG* Count) override; IFACEMETHOD(TagImage)(_In_ const WSLCTagImageOptions* Options) override; - IFACEMETHOD(PushImage)(_In_ LPCSTR Image, _In_opt_ LPCSTR RegistryAuthenticationInformation, _In_opt_ IProgressCallback* ProgressCallback) override; + IFACEMETHOD(PushImage)(_In_ LPCSTR Image, _In_ LPCSTR RegistryAuthenticationInformation, _In_opt_ IProgressCallback* ProgressCallback) override; IFACEMETHOD(InspectImage)(_In_ LPCSTR ImageNameOrId, _Out_ LPSTR* Output) override; IFACEMETHOD(Authenticate)(_In_ LPCSTR ServerAddress, _In_ LPCSTR Username, _In_ LPCSTR Password, _Out_ LPSTR* IdentityToken) override; From a17819e7ee09d3069f0f1c0033d0fc05e5c574a7 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 18:24:03 -0700 Subject: [PATCH 15/76] Address suggestions --- src/windows/wslcsession/DockerHTTPClient.cpp | 2 +- src/windows/wslcsession/DockerHTTPClient.h | 2 +- src/windows/wslcsession/WSLCSession.cpp | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 3ac26a5fe..45e469db5 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -172,7 +172,7 @@ void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, } std::unique_ptr DockerHTTPClient::PushImage( - const std::string& ImageName, const std::optional& tag, std::string& registryAuth) + const std::string& ImageName, const std::optional& tag, const std::string& registryAuth) { auto url = URL::Create("/images/{}/push", ImageName); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index 9f448d23d..f9480ccd5 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -156,7 +156,7 @@ class DockerHTTPClient std::unique_ptr ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength); std::unique_ptr LoadImage(uint64_t ContentLength); void TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag); - std::unique_ptr PushImage(const std::string& ImageName, const std::optional& tag, std::string& registryAuth); + std::unique_ptr PushImage(const std::string& ImageName, const std::optional& tag, const std::string& registryAuth); std::string Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password); std::vector ListImages(bool all = false, bool digests = false, const ListImagesFilters& filters = {}); common::docker_schema::InspectImage InspectImage(const std::string& NameOrId); diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 2f39ed9d8..c1c9d9208 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -1303,8 +1303,7 @@ try THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value()); auto [repo, tagOrDigest] = wslutil::ParseImage(Image); - std::string registryAuth = RegistryAuthenticationInformation; - auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, registryAuth); + auto requestContext = m_dockerClient->PushImage(repo, tagOrDigest, RegistryAuthenticationInformation); StreamImageOperation(*requestContext, Image, "Push", ProgressCallback); return S_OK; From e329ac7ba208392c75bd299b44b367b02e5e0e2d Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 6 Apr 2026 23:51:50 -0700 Subject: [PATCH 16/76] address suggestions --- src/windows/common/WSLCLocalRegistry.cpp | 34 +++++++----------------- src/windows/common/WSLCLocalRegistry.h | 18 +++++++++---- test/windows/WSLCTests.cpp | 15 +++++------ 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/windows/common/WSLCLocalRegistry.cpp b/src/windows/common/WSLCLocalRegistry.cpp index 666d1f3b2..48f67aeb2 100644 --- a/src/windows/common/WSLCLocalRegistry.cpp +++ b/src/windows/common/WSLCLocalRegistry.cpp @@ -21,10 +21,10 @@ namespace { constexpr auto c_registryImage = "wslc-registry:latest"; -std::vector BuildRegistryEnv(const std::string& username, const std::string& password) +std::vector BuildRegistryEnv(const std::string& username, const std::string& password, USHORT port) { std::vector env = { - "REGISTRY_HTTP_ADDR=0.0.0.0:5000", + std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port), }; if (!username.empty()) @@ -38,10 +38,12 @@ std::vector BuildRegistryEnv(const std::string& username, const std } // namespace -WSLCLocalRegistry::WSLCLocalRegistry(IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password) : +WSLCLocalRegistry::WSLCLocalRegistry( + IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password, std::string&& serverAddress) : m_session(wil::com_ptr(&session)), m_username(std::move(username)), m_password(std::move(password)), + m_serverAddress(std::move(serverAddress)), m_container(std::move(container)) { } @@ -52,30 +54,14 @@ WSLCLocalRegistry::~WSLCLocalRegistry() m_container.Reset(); } -WSLCLocalRegistry WSLCLocalRegistry::Start(IWSLCSession& session, const std::string& username, const std::string& password) +WSLCLocalRegistry WSLCLocalRegistry::Start(IWSLCSession& session, const std::string& username, const std::string& password, USHORT port) { - auto env = BuildRegistryEnv(username, password); + auto env = BuildRegistryEnv(username, password, port); WSLCContainerLauncher launcher(c_registryImage, {}, {}, env); launcher.SetEntrypoint({"/entrypoint.sh"}); - launcher.AddPort(5000, 5000, AF_INET); + launcher.AddPort(port, port, AF_INET); auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); - - return WSLCLocalRegistry(session, std::move(container), std::string(username), std::string(password)); -} - -const char* WSLCLocalRegistry::GetServerAddress() const -{ - return "127.0.0.1:5000"; -} - -const std::string& WSLCLocalRegistry::GetUsername() const -{ - return m_username; -} - -const std::string& WSLCLocalRegistry::GetPassword() const -{ - return m_password; -} + return WSLCLocalRegistry(session, std::move(container), std::string(username), std::string(password), std::format("127.0.0.1:{}", port)); +} \ No newline at end of file diff --git a/src/windows/common/WSLCLocalRegistry.h b/src/windows/common/WSLCLocalRegistry.h index 3c0fd079d..58efcebbe 100644 --- a/src/windows/common/WSLCLocalRegistry.h +++ b/src/windows/common/WSLCLocalRegistry.h @@ -17,6 +17,7 @@ Module Name: #pragma once #include "WSLCContainerLauncher.h" +#include "WslcCredentialStore.h" namespace wsl::windows::common { @@ -27,16 +28,23 @@ class WSLCLocalRegistry DEFAULT_MOVABLE(WSLCLocalRegistry); ~WSLCLocalRegistry(); - static WSLCLocalRegistry Start(IWSLCSession& Session, const std::string& Username = {}, const std::string& Password = {}); + static WSLCLocalRegistry Start(IWSLCSession& Session, const std::string& Username = {}, const std::string& Password = {}, USHORT Port = 5000); - const char* GetServerAddress() const; - const std::string& GetUsername() const; - const std::string& GetPassword() const; + std::string GetServerAddress() + { + return m_serverAddress; + } + + std::string GetAuthHeader() + { + return BuildRegistryAuthHeader(m_username, m_password, m_serverAddress); + } private: - WSLCLocalRegistry(IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password); + WSLCLocalRegistry(IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password, std::string&& serverAddress); wil::com_ptr m_session; + std::string m_serverAddress; std::string m_username; std::string m_password; RunningWSLCContainer m_container; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index fe83d85b5..266e7d6cb 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -466,8 +466,7 @@ class WSLCTests auto registryUrl = std::format(L"http://{}/v2/", registryAddress); ExpectHttpResponse(registryUrl.c_str(), 200, true); - auto auth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); - PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, auth); + PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, registry.GetAuthHeader()); auto image = std::format("{}/hello-world:latest", registryAddress); @@ -517,6 +516,7 @@ class WSLCTests // Start a local registry without auth to avoid Docker Hub rate limits. auto registry = wsl::windows::common::WSLCLocalRegistry::Start(*m_defaultSession); auto registryAddress = registry.GetServerAddress(); + auto auth = registry.GetAuthHeader(); // Wait for the registry to be ready. auto registryUrl = std::format(L"http://{}/v2/", registryAddress); @@ -524,7 +524,6 @@ class WSLCTests auto validatePull = [&](const std::string& sourceImage, const std::string& registryTag) { // Push the source image to the local registry. - auto auth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); PushImageToRegistry(*m_defaultSession, sourceImage, registryAddress, auth); auto registryImage = std::format("{}/{}", registryAddress, registryTag); @@ -553,7 +552,7 @@ class WSLCTests // Validate that pushing a non-existent image fails. { - VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", nullptr, nullptr), E_INVALIDARG); + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", "", nullptr), E_INVALIDARG); } // Validate that PushImage() returns the appropriate error if the session is terminated. @@ -562,7 +561,7 @@ class WSLCTests auto cleanup = wil::scope_exit([&]() { ResetTestSession(); }); - VERIFY_ARE_EQUAL(m_defaultSession->PushImage("hello-world:latest", nullptr, nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("hello-world:latest", "", nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } } @@ -582,13 +581,13 @@ class WSLCTests ExpectHttpResponse(registryUrl.c_str(), 401, true); wil::unique_cotaskmem_ansistring token; - VERIFY_ARE_EQUAL(m_defaultSession->Authenticate(registryAddress, c_username, "wrong-password", &token), E_FAIL); + VERIFY_ARE_EQUAL(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, "wrong-password", &token), E_FAIL); ValidateCOMErrorMessageContains(L"failed with status: 401 Unauthorized"); - VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress, c_username, c_password, &token)); + VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, c_password, &token)); VERIFY_IS_NOT_NULL(token.get()); - auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); + auto xRegistryAuth = registry.GetAuthHeader(); PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, xRegistryAuth); // Pulling without credentials should fail. From 3256f5de3e0b738b6a6de3a9221e3216d4868b34 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 10:41:15 -0700 Subject: [PATCH 17/76] Address copilot comments --- src/windows/common/WSLCContainerLauncher.cpp | 2 ++ src/windows/common/WSLCLocalRegistry.cpp | 2 ++ src/windows/common/WslcCredentialStore.cpp | 2 +- src/windows/service/inc/wslc.idl | 5 +++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index 4d0744b9c..78eb56403 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -11,6 +11,8 @@ Module Name: This file contains the implementation for WSLCContainerLauncher. --*/ + +#include "precomp.h" #include "WSLCContainerLauncher.h" using wsl::windows::common::ClientRunningWSLCProcess; diff --git a/src/windows/common/WSLCLocalRegistry.cpp b/src/windows/common/WSLCLocalRegistry.cpp index 48f67aeb2..9a1c76c8b 100644 --- a/src/windows/common/WSLCLocalRegistry.cpp +++ b/src/windows/common/WSLCLocalRegistry.cpp @@ -11,6 +11,8 @@ Module Name: Implementation of WSLCLocalRegistry. --*/ + +#include "precomp.h" #include "WSLCLocalRegistry.h" using wsl::windows::common::RunningWSLCContainer; diff --git a/src/windows/common/WslcCredentialStore.cpp b/src/windows/common/WslcCredentialStore.cpp index a66b6587c..9f85b139d 100644 --- a/src/windows/common/WslcCredentialStore.cpp +++ b/src/windows/common/WslcCredentialStore.cpp @@ -12,8 +12,8 @@ Module Name: --*/ +#include "precomp.h" #include "WslcCredentialStore.h" -#include #include namespace { diff --git a/src/windows/service/inc/wslc.idl b/src/windows/service/inc/wslc.idl index 3d3b4be98..9ea523dd5 100644 --- a/src/windows/service/inc/wslc.idl +++ b/src/windows/service/inc/wslc.idl @@ -591,9 +591,7 @@ interface IWSLCSession : IUnknown HRESULT ListImages([in, unique] const WSLCListImageOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count); HRESULT DeleteImage([in] const WSLCDeleteImageOptions* Options, [out, size_is(, *Count)] WSLCDeletedImageInformation** DeletedImages, [out] ULONG* Count); HRESULT TagImage([in] const WSLCTagImageOptions* Options); - HRESULT PushImage([in] LPCSTR Image, [in] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); HRESULT InspectImage([in] LPCSTR ImageNameOrId, [out] LPSTR* Output); - HRESULT Authenticate([in] LPCSTR ServerAddress, [in] LPCSTR Username, [in] LPCSTR Password, [out] LPSTR* IdentityToken); // Container management. HRESULT CreateContainer([in] const WSLCContainerOptions* Options, [out] IWSLCContainer** Container); @@ -635,6 +633,9 @@ interface IWSLCSession : IUnknown HRESULT DeleteVolume([in] LPCSTR Name); HRESULT ListVolumes([out, size_is(, *Count)] WSLCVolumeInformation** Volumes, [out] ULONG* Count); HRESULT InspectVolume([in] LPCSTR Name, [out] LPSTR* Output); + + HRESULT Authenticate([in] LPCSTR ServerAddress, [in] LPCSTR Username, [in] LPCSTR Password, [out] LPSTR* IdentityToken); + HRESULT PushImage([in] LPCSTR Image, [in] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback); } // From 9177765fce32a2a2e7c4251f39524bbd15eeac1e Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 11:50:32 -0700 Subject: [PATCH 18/76] Remove wslc local registry --- src/windows/common/CMakeLists.txt | 2 - src/windows/common/WSLCLocalRegistry.cpp | 69 ------------------------ src/windows/common/WSLCLocalRegistry.h | 53 ------------------ test/windows/WSLCTests.cpp | 37 ++++++++----- 4 files changed, 25 insertions(+), 136 deletions(-) delete mode 100644 src/windows/common/WSLCLocalRegistry.cpp delete mode 100644 src/windows/common/WSLCLocalRegistry.h diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index 99a9efc55..bb54a7e09 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -35,7 +35,6 @@ set(SOURCES SubProcess.cpp svccomm.cpp WSLCContainerLauncher.cpp - WSLCLocalRegistry.cpp WslcCredentialStore.cpp VirtioNetworking.cpp WSLCProcessLauncher.cpp @@ -118,7 +117,6 @@ set(HEADERS SubProcess.h svccomm.hpp WSLCContainerLauncher.h - WSLCLocalRegistry.h WslcCredentialStore.h VirtioNetworking.h WSLCProcessLauncher.h diff --git a/src/windows/common/WSLCLocalRegistry.cpp b/src/windows/common/WSLCLocalRegistry.cpp deleted file mode 100644 index 9a1c76c8b..000000000 --- a/src/windows/common/WSLCLocalRegistry.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/*++ - -Copyright (c) Microsoft. All rights reserved. - -Module Name: - - WSLCLocalRegistry.cpp - -Abstract: - - Implementation of WSLCLocalRegistry. - ---*/ - -#include "precomp.h" -#include "WSLCLocalRegistry.h" - -using wsl::windows::common::RunningWSLCContainer; -using wsl::windows::common::WSLCContainerLauncher; -using wsl::windows::common::WSLCLocalRegistry; - -namespace { - -constexpr auto c_registryImage = "wslc-registry:latest"; - -std::vector BuildRegistryEnv(const std::string& username, const std::string& password, USHORT port) -{ - std::vector env = { - std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port), - }; - - if (!username.empty()) - { - env.push_back(std::format("USERNAME={}", username)); - env.push_back(std::format("PASSWORD={}", password)); - } - - return env; -} - -} // namespace - -WSLCLocalRegistry::WSLCLocalRegistry( - IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password, std::string&& serverAddress) : - m_session(wil::com_ptr(&session)), - m_username(std::move(username)), - m_password(std::move(password)), - m_serverAddress(std::move(serverAddress)), - m_container(std::move(container)) -{ -} - -WSLCLocalRegistry::~WSLCLocalRegistry() -{ - // Delete the container first while the session is still active. - m_container.Reset(); -} - -WSLCLocalRegistry WSLCLocalRegistry::Start(IWSLCSession& session, const std::string& username, const std::string& password, USHORT port) -{ - auto env = BuildRegistryEnv(username, password, port); - - WSLCContainerLauncher launcher(c_registryImage, {}, {}, env); - launcher.SetEntrypoint({"/entrypoint.sh"}); - launcher.AddPort(port, port, AF_INET); - - auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); - return WSLCLocalRegistry(session, std::move(container), std::string(username), std::string(password), std::format("127.0.0.1:{}", port)); -} \ No newline at end of file diff --git a/src/windows/common/WSLCLocalRegistry.h b/src/windows/common/WSLCLocalRegistry.h deleted file mode 100644 index 58efcebbe..000000000 --- a/src/windows/common/WSLCLocalRegistry.h +++ /dev/null @@ -1,53 +0,0 @@ -/*++ - -Copyright (c) Microsoft. All rights reserved. - -Module Name: - - WSLCLocalRegistry.h - -Abstract: - - Helper class that starts a local Docker registry:3 container inside a WSLC - session, optionally configured with htpasswd basic authentication. Intended - for use in both unit tests and E2E tests that need a private registry without - an external dependency. - ---*/ - -#pragma once -#include "WSLCContainerLauncher.h" -#include "WslcCredentialStore.h" - -namespace wsl::windows::common { - -class WSLCLocalRegistry -{ -public: - NON_COPYABLE(WSLCLocalRegistry); - DEFAULT_MOVABLE(WSLCLocalRegistry); - ~WSLCLocalRegistry(); - - static WSLCLocalRegistry Start(IWSLCSession& Session, const std::string& Username = {}, const std::string& Password = {}, USHORT Port = 5000); - - std::string GetServerAddress() - { - return m_serverAddress; - } - - std::string GetAuthHeader() - { - return BuildRegistryAuthHeader(m_username, m_password, m_serverAddress); - } - -private: - WSLCLocalRegistry(IWSLCSession& session, RunningWSLCContainer&& container, std::string&& username, std::string&& password, std::string&& serverAddress); - - wil::com_ptr m_session; - std::string m_serverAddress; - std::string m_username; - std::string m_password; - RunningWSLCContainer m_container; -}; - -} // namespace wsl::windows::common diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index fecfb04e4..7eaba4b9f 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -17,7 +17,6 @@ Module Name: #include "wslc.h" #include "WSLCProcessLauncher.h" #include "WSLCContainerLauncher.h" -#include "WSLCLocalRegistry.h" #include "WslcCredentialStore.h" #include "WslCoreFilesystem.h" @@ -184,7 +183,24 @@ class WSLCTests return RunningWSLCContainer(std::move(rawContainer), {}); } - void PushImageToRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth) + std::pair StartLocalRegistry(const std::string& username = {}, const std::string& password = {}, USHORT port = 5000) + { + std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; + if (!username.empty()) + { + env.push_back(std::format("USERNAME={}", username)); + env.push_back(std::format("PASSWORD={}", password)); + } + + WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env); + launcher.SetEntrypoint({"/entrypoint.sh"}); + launcher.AddPort(port, port, AF_INET); + + auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone); + return {std::move(container), std::format("127.0.0.1:{}", port)}; + } + + static void PushImageToRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth) { auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest")); @@ -459,14 +475,13 @@ class WSLCTests { // Start a local registry without auth and push hello-world:latest to it. - auto registry = wsl::windows::common::WSLCLocalRegistry::Start(*m_defaultSession); - auto registryAddress = registry.GetServerAddress(); + auto [registryContainer, registryAddress] = StartLocalRegistry(); // Wait for the registry to be ready. auto registryUrl = std::format(L"http://{}/v2/", registryAddress); ExpectHttpResponse(registryUrl.c_str(), 200, true); - PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, registry.GetAuthHeader()); + PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress)); auto image = std::format("{}/hello-world:latest", registryAddress); @@ -514,9 +529,8 @@ class WSLCTests WSL2_TEST_ONLY(); // Start a local registry without auth to avoid Docker Hub rate limits. - auto registry = wsl::windows::common::WSLCLocalRegistry::Start(*m_defaultSession); - auto registryAddress = registry.GetServerAddress(); - auto auth = registry.GetAuthHeader(); + auto [registryContainer, registryAddress] = StartLocalRegistry(); + auto auth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); // Wait for the registry to be ready. auto registryUrl = std::format(L"http://{}/v2/", registryAddress); @@ -572,10 +586,9 @@ class WSLCTests constexpr auto c_username = "wslctest"; constexpr auto c_password = "password"; - auto registry = wsl::windows::common::WSLCLocalRegistry::Start(*m_defaultSession, c_username, c_password); + auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password); - auto registryUrl = std::format(L"http://{}/v2/", registry.GetServerAddress()); - auto registryAddress = registry.GetServerAddress(); + auto registryUrl = std::format(L"http://{}/v2/", registryAddress); // The registry may take some time before it's up and running. Retry until it's ready to accept connections. ExpectHttpResponse(registryUrl.c_str(), 401, true); @@ -587,7 +600,7 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, c_password, &token)); VERIFY_IS_NOT_NULL(token.get()); - auto xRegistryAuth = registry.GetAuthHeader(); + auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, xRegistryAuth); // Pulling without credentials should fail. From 0fa5505da73eee3bdffba56239c9363397f7b6d8 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 12:19:29 -0700 Subject: [PATCH 19/76] formatting fix --- test/windows/WSLCTests.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 7eaba4b9f..23ea00664 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -481,7 +481,8 @@ class WSLCTests auto registryUrl = std::format(L"http://{}/v2/", registryAddress); ExpectHttpResponse(registryUrl.c_str(), 200, true); - PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress)); + PushImageToRegistry( + *m_defaultSession, "hello-world:latest", registryAddress, wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress)); auto image = std::format("{}/hello-world:latest", registryAddress); From 7b41634c5ed636466d7f9a3ceb5e81368e370eeb Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 15:44:16 -0700 Subject: [PATCH 20/76] Add script and docker file to generate test images. --- tools/test/images/build-image.ps1 | 43 +++++++++++++++++++ tools/test/images/wslc-registry/Dockerfile | 8 ++++ tools/test/images/wslc-registry/entrypoint.sh | 13 ++++++ 3 files changed, 64 insertions(+) create mode 100644 tools/test/images/build-image.ps1 create mode 100644 tools/test/images/wslc-registry/Dockerfile create mode 100644 tools/test/images/wslc-registry/entrypoint.sh diff --git a/tools/test/images/build-image.ps1 b/tools/test/images/build-image.ps1 new file mode 100644 index 000000000..b7d275798 --- /dev/null +++ b/tools/test/images/build-image.ps1 @@ -0,0 +1,43 @@ +<# +.SYNOPSIS + Builds a custom test registry image using wslc and saves it as a .tar file. +.DESCRIPTION + This script builds a custom image using wslc from a specified Dockerfile and saves the resulting image as a .tar file. + This is useful for preparing test images for WSL container tests. +.PARAMETER Dockerfile + Path to the Dockerfile to build. +.PARAMETER ImageTag + Tag for the built image. +.PARAMETER OutputFile + Path to save the exported .tar file. +#> + +[CmdletBinding(SupportsShouldProcess)] +param ( + [string]$DockerfileDir, + [string]$ImageTag, + [string]$OutputFile = "" +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +if ($OutputFile -eq "") { + $OutputFile = Join-Path $PWD "$(Split-Path -Leaf $DockerfileDir).tar" +} + +# Verify $OutputFile is a valid path, we can write to it, and that it has a .tar extension +if ([System.IO.Path]::GetExtension($OutputFile) -ne ".tar") { + if (-not $PSCmdlet.ShouldContinue("Are you sure you want to continue?", "Output file '$OutputFile' is not a .tar file.")) { + throw "Aborting due to invalid output file extension." + } +} + + +& wslc build -t $ImageTag $DockerfileDir +if ($LASTEXITCODE -ne 0) { throw "wslc build failed with exit code $LASTEXITCODE" } + +& wslc save --output $OutputFile $ImageTag +if ($LASTEXITCODE -ne 0) { throw "wslc save failed with exit code $LASTEXITCODE" } + +Write-Host "Image built and saved to $OutputFile successfully." diff --git a/tools/test/images/wslc-registry/Dockerfile b/tools/test/images/wslc-registry/Dockerfile new file mode 100644 index 000000000..6d5084810 --- /dev/null +++ b/tools/test/images/wslc-registry/Dockerfile @@ -0,0 +1,8 @@ +FROM registry:3 + +RUN apk add --no-cache apache2-utils + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/tools/test/images/wslc-registry/entrypoint.sh b/tools/test/images/wslc-registry/entrypoint.sh new file mode 100644 index 000000000..08333fab4 --- /dev/null +++ b/tools/test/images/wslc-registry/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +if [ -n "$USERNAME" ]; then + mkdir -p /auth + htpasswd -Bbn "$USERNAME" "$PASSWORD" > /auth/htpasswd + + export REGISTRY_AUTH=htpasswd + export REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd + export REGISTRY_AUTH_HTPASSWD_REALM="WSLC Registry" +fi + +exec registry serve /etc/distribution/config.yml From aaed20ec9778263e2649bdff5d8e7763c5aeb142 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 15:48:11 -0700 Subject: [PATCH 21/76] Address base64 encode feedback --- src/windows/common/WslcCredentialStore.cpp | 27 ++---------------- src/windows/common/wslutil.cpp | 32 ++++++++++++++++++++++ src/windows/common/wslutil.h | 3 ++ src/windows/wslcsession/WSLCSession.cpp | 17 +----------- 4 files changed, 39 insertions(+), 40 deletions(-) diff --git a/src/windows/common/WslcCredentialStore.cpp b/src/windows/common/WslcCredentialStore.cpp index 9f85b139d..e15266ec3 100644 --- a/src/windows/common/WslcCredentialStore.cpp +++ b/src/windows/common/WslcCredentialStore.cpp @@ -14,39 +14,18 @@ Module Name: #include "precomp.h" #include "WslcCredentialStore.h" -#include - -namespace { - -std::string Base64Encode(const std::string& input) -{ - DWORD base64Size = 0; - THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( - reinterpret_cast(input.c_str()), static_cast(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &base64Size)); - - auto buffer = std::make_unique(base64Size); - THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( - reinterpret_cast(input.c_str()), - static_cast(input.size()), - CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, - buffer.get(), - &base64Size)); - - return std::string(buffer.get()); -} - -} // namespace +#include "wslutil.h" std::string wsl::windows::common::BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress) { nlohmann::json authJson = {{"username", username}, {"password", password}, {"serveraddress", serverAddress}}; - return Base64Encode(authJson.dump()); + return wslutil::Base64Encode(authJson.dump()); } std::string wsl::windows::common::BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress) { nlohmann::json authJson = {{"identitytoken", identityToken}, {"serveraddress", serverAddress}}; - return Base64Encode(authJson.dump()); + return wslutil::Base64Encode(authJson.dump()); } diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 7c11df0e4..b6d2e5f14 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -15,6 +15,7 @@ Module Name: #include "precomp.h" #include "wslutil.h" #include "WslPluginApi.h" +#include #include "wslinstallerservice.h" #include "wslc.h" @@ -1412,4 +1413,35 @@ catch (...) { LOG_CAUGHT_EXCEPTION(); return nullptr; +} + +std::string wsl::windows::common::wslutil::Base64Encode(const std::string& input) +{ + DWORD base64Size = 0; + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( + reinterpret_cast(input.c_str()), static_cast(input.size()), CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &base64Size)); + + auto buffer = std::make_unique(base64Size); + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA( + reinterpret_cast(input.c_str()), + static_cast(input.size()), + CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, + buffer.get(), + &base64Size)); + + return std::string(buffer.get()); +} + +std::string wsl::windows::common::wslutil::Base64Decode(const std::string& encoded) +{ + DWORD size = 0; + THROW_IF_WIN32_BOOL_FALSE(CryptStringToBinaryA( + encoded.c_str(), static_cast(encoded.size()), CRYPT_STRING_BASE64, nullptr, &size, nullptr, nullptr)); + + std::string result(size, '\0'); + THROW_IF_WIN32_BOOL_FALSE(CryptStringToBinaryA( + encoded.c_str(), static_cast(encoded.size()), CRYPT_STRING_BASE64, reinterpret_cast(result.data()), &size, nullptr, nullptr)); + + result.resize(size); + return result; } \ No newline at end of file diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index babbbe2ff..3318d8af1 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -323,4 +323,7 @@ WSLCHandle ToCOMInputHandle(HANDLE Handle); winrt::Windows::Management::Deployment::PackageVolume GetSystemVolume(); +std::string Base64Encode(const std::string& input); +std::string Base64Decode(const std::string& encoded); + } // namespace wsl::windows::common::wslutil diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 6ffe7d7e1..5a1e962a1 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -15,7 +15,6 @@ Module Name: #include "precomp.h" #include "WSLCSession.h" #include "WSLCContainer.h" -#include #include "ServiceProcessLauncher.h" #include "WslCoreFilesystem.h" @@ -30,20 +29,6 @@ constexpr auto c_containerdStorage = "/var/lib/docker"; namespace { -std::string Base64Decode(const std::string& encoded) -{ - DWORD size = 0; - THROW_IF_WIN32_BOOL_FALSE(CryptStringToBinaryA( - encoded.c_str(), static_cast(encoded.size()), CRYPT_STRING_BASE64, nullptr, &size, nullptr, nullptr)); - - std::string result(size, '\0'); - THROW_IF_WIN32_BOOL_FALSE(CryptStringToBinaryA( - encoded.c_str(), static_cast(encoded.size()), CRYPT_STRING_BASE64, reinterpret_cast(result.data()), &size, nullptr, nullptr)); - - result.resize(size); - return result; -} - // Resolve \r overwrites: for each \n-delimited line, keep only the content after the last \r. // This collapses terminal progress updates (e.g. "50%\r75%\r100%") to their final state. std::string ResolveCarriageReturns(const std::string& input) @@ -700,7 +685,7 @@ try continue; } - std::string decoded = Base64Decode(log.data); + std::string decoded = wslutil::Base64Decode(log.data); if (!decoded.empty()) { auto& logBuffer = vertexLogs[log.vertex]; From 65aed10ff3e2669283f22959d7086efc9f41998a Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 15:48:24 -0700 Subject: [PATCH 22/76] Add Auth to SDK --- CMakeLists.txt | 1 + src/windows/WslcSDK/WslcsdkPrivate.h | 2 + src/windows/WslcSDK/wslcsdk.cpp | 92 ++++++- src/windows/WslcSDK/wslcsdk.def | 4 + src/windows/WslcSDK/wslcsdk.h | 63 ++++- test/windows/WslcSdkTests.cpp | 355 +++++++++++++++++++++------ 6 files changed, 427 insertions(+), 90 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7119eafcc..1400001af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -285,6 +285,7 @@ set(COMMON_LINK_LIBRARIES Shlwapi.lib synchronization.lib Bcrypt.lib + Crypt32.lib icu.lib) set(MSI_LINK_LIBRARIES diff --git a/src/windows/WslcSDK/WslcsdkPrivate.h b/src/windows/WslcSDK/WslcsdkPrivate.h index 339d7f855..d5d38d58c 100644 --- a/src/windows/WslcSDK/WslcsdkPrivate.h +++ b/src/windows/WslcSDK/WslcsdkPrivate.h @@ -89,6 +89,8 @@ typedef struct WslcContainerOptionsInternal const WslcContainerProcessOptionsInternal* initProcessOptions; WSLCContainerNetworkType networking; WslcContainerFlags containerFlags; + PCSTR const* entrypoint; + uint32_t entrypointCount; } WslcContainerOptionsInternal; diff --git a/src/windows/WslcSDK/wslcsdk.cpp b/src/windows/WslcSDK/wslcsdk.cpp index e061370ba..37db2b40c 100644 --- a/src/windows/WslcSDK/wslcsdk.cpp +++ b/src/windows/WslcSDK/wslcsdk.cpp @@ -684,8 +684,13 @@ try containerOptions.ContainerNetwork.ContainerNetworkType = internalContainerSettings->networking; + if (internalContainerSettings->entrypoint && internalContainerSettings->entrypointCount) + { + containerOptions.Entrypoint.Values = internalContainerSettings->entrypoint; + containerOptions.Entrypoint.Count = internalContainerSettings->entrypointCount; + } + // TODO: No user access - // containerOptions.Entrypoint; // containerOptions.Labels; // containerOptions.LabelsCount; // containerOptions.StopSignal; @@ -745,6 +750,22 @@ try } CATCH_RETURN(); +STDAPI WslcSetContainerSettingsEntrypoint(_In_ WslcContainerSettings* containerSettings, _In_reads_(argc) PCSTR const* argv, _In_ size_t argc) +try +{ + auto internalType = CheckAndGetInternalType(containerSettings); + RETURN_HR_IF( + E_INVALIDARG, + (argv == nullptr && argc != 0) || (argv != nullptr && argc == 0) || + (argc > static_cast(std::numeric_limits::max()))); + + internalType->entrypoint = argv; + internalType->entrypointCount = static_cast(argc); + + return S_OK; +} +CATCH_RETURN(); + STDAPI WslcSetContainerSettingsName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR name) try { @@ -1186,8 +1207,7 @@ try auto progressCallback = ProgressCallback::CreateIf(options); - // TODO: Auth - return errorInfoWrapper.CaptureResult(internalType->session->PullImage(options->uri, nullptr, progressCallback.get())); + return errorInfoWrapper.CaptureResult(internalType->session->PullImage(options->uri, options->registryAuth, progressCallback.get())); } CATCH_RETURN(); @@ -1282,6 +1302,72 @@ try } CATCH_RETURN(); +STDAPI WslcTagSessionImage(_In_ WslcSession session, _In_ const WslcTagImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage) +try +{ + ErrorInfoWrapper errorInfoWrapper{errorMessage}; + auto internalType = CheckAndGetInternalType(session); + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session); + RETURN_HR_IF_NULL(E_POINTER, options); + RETURN_HR_IF_NULL(E_INVALIDARG, options->image); + RETURN_HR_IF_NULL(E_INVALIDARG, options->repo); + RETURN_HR_IF_NULL(E_INVALIDARG, options->tag); + + WSLCTagImageOptions runtimeOptions{}; + runtimeOptions.Image = options->image; + runtimeOptions.Repo = options->repo; + runtimeOptions.Tag = options->tag; + + return errorInfoWrapper.CaptureResult(internalType->session->TagImage(&runtimeOptions)); +} +CATCH_RETURN(); + +STDAPI WslcPushSessionImage(_In_ WslcSession session, _In_ const WslcPushImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage) +try +{ + ErrorInfoWrapper errorInfoWrapper{errorMessage}; + auto internalType = CheckAndGetInternalType(session); + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session); + RETURN_HR_IF_NULL(E_POINTER, options); + RETURN_HR_IF_NULL(E_INVALIDARG, options->image); + + auto progressCallback = ProgressCallback::CreateIf(options); + + return errorInfoWrapper.CaptureResult( + internalType->session->PushImage(options->image, options->registryAuth ? options->registryAuth : "", progressCallback.get())); +} +CATCH_RETURN(); + +STDAPI WslcSessionAuthenticate( + _In_ WslcSession session, + _In_z_ PCSTR serverAddress, + _In_z_ PCSTR username, + _In_z_ PCSTR password, + _Outptr_result_z_ PSTR* identityToken, + _Outptr_opt_result_z_ PWSTR* errorMessage) +try +{ + ErrorInfoWrapper errorInfoWrapper{errorMessage}; + auto internalType = CheckAndGetInternalType(session); + RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session); + RETURN_HR_IF_NULL(E_POINTER, serverAddress); + RETURN_HR_IF_NULL(E_POINTER, username); + RETURN_HR_IF_NULL(E_POINTER, password); + RETURN_HR_IF_NULL(E_POINTER, identityToken); + + *identityToken = nullptr; + + wil::unique_cotaskmem_ansistring token; + auto hr = errorInfoWrapper.CaptureResult(internalType->session->Authenticate(serverAddress, username, password, &token)); + if (SUCCEEDED(hr)) + { + *identityToken = token.release(); + } + + return errorInfoWrapper; +} +CATCH_RETURN(); + STDAPI WslcListSessionImages(_In_ WslcSession session, _Outptr_result_buffer_(*count) WslcImageInfo** images, _Out_ uint32_t* count) try { diff --git a/src/windows/WslcSDK/wslcsdk.def b/src/windows/WslcSDK/wslcsdk.def index 63e60a436..e3acd91f5 100644 --- a/src/windows/WslcSDK/wslcsdk.def +++ b/src/windows/WslcSDK/wslcsdk.def @@ -23,6 +23,7 @@ WslcSetSessionSettingsTimeout WslcSetSessionSettingsVhd WslcTerminateSession +WslcSessionAuthenticate WslcPullSessionImage WslcImportSessionImage WslcImportSessionImageFromFile @@ -32,6 +33,8 @@ WslcDeleteSessionImage WslcListSessionImages WslcCreateSessionVhdVolume WslcDeleteSessionVhdVolume +WslcTagSessionImage +WslcPushSessionImage WslcSetContainerSettingsDomainName WslcSetContainerSettingsName @@ -42,6 +45,7 @@ WslcSetContainerSettingsNamedVolumes WslcSetContainerSettingsInitProcess WslcSetContainerSettingsFlags WslcSetContainerSettingsPortMappings +WslcSetContainerSettingsEntrypoint WslcCreateContainerProcess WslcStartContainer diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index a581afd7f..fe70320e9 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -32,7 +32,7 @@ typedef struct WslcSessionSettings DECLARE_HANDLE(WslcSession); // Container values -#define WSLC_CONTAINER_OPTIONS_SIZE 96 +#define WSLC_CONTAINER_OPTIONS_SIZE 112 #define WSLC_CONTAINER_OPTIONS_ALIGNMENT 8 typedef struct WslcContainerSettings @@ -180,6 +180,8 @@ STDAPI WslcSetContainerSettingsDomainName(_In_ WslcContainerSettings* containerS STDAPI WslcSetContainerSettingsFlags(_In_ WslcContainerSettings* containerSettings, _In_ WslcContainerFlags flags); +STDAPI WslcSetContainerSettingsEntrypoint(_In_ WslcContainerSettings* containerSettings, _In_reads_(argc) PCSTR const* argv, _In_ size_t argc); + STDAPI WslcSetContainerSettingsPortMappings( _In_ WslcContainerSettings* containerSettings, _In_reads_opt_(portMappingCount) const WslcContainerPortMapping* portMappings, @@ -387,11 +389,6 @@ typedef struct WslcImageProgressMessage _Out_ WslcImageProgressDetail detail; } WslcImageProgressMessage; -typedef struct WslcRegistryAuthenticationInformation -{ - // TBD -} WslcRegistryAuthenticationInformation; - // pointer-to-function typedef (unambiguous) typedef HRESULT(CALLBACK* WslcContainerImageProgressCallback)(const WslcImageProgressMessage* progress, PVOID context); @@ -401,7 +398,7 @@ typedef struct WslcPullImageOptions _In_z_ PCSTR uri; WslcContainerImageProgressCallback progressCallback; PVOID progressCallbackContext; - _In_opt_ const WslcRegistryAuthenticationInformation* authInfo; + _In_opt_z_ PCSTR registryAuth; } WslcPullImageOptions; STDAPI WslcPullSessionImage(_In_ WslcSession session, _In_ const WslcPullImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage); @@ -452,6 +449,58 @@ typedef struct WslcImageInfo STDAPI WslcDeleteSessionImage(_In_ WslcSession session, _In_z_ PCSTR NameOrId, _Outptr_opt_result_z_ PWSTR* errorMessage); +typedef struct WslcTagImageOptions +{ + _In_z_ PCSTR image; // Source image name or ID. + _In_z_ PCSTR repo; // Target repository name. + _In_z_ PCSTR tag; // Target tag name. +} WslcTagImageOptions; + +STDAPI WslcTagSessionImage(_In_ WslcSession session, _In_ const WslcTagImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage); + +typedef struct WslcPushImageOptions +{ + _In_z_ PCSTR image; + _In_opt_z_ PCSTR registryAuth; // Base64-encoded X-Registry-Auth header value. + _In_opt_ WslcContainerImageProgressCallback progressCallback; + _In_opt_ PVOID progressCallbackContext; +} WslcPushImageOptions; + +STDAPI WslcPushSessionImage(_In_ WslcSession session, _In_ const WslcPushImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage); + +// Authenticates with a container registry and returns an identity token. +// +// Parameters: +// session +// A valid WslcSession handle. +// +// serverAddress +// The registry server address (e.g. "127.0.0.1:5000"). +// +// username +// The username for authentication. +// +// password +// The password for authentication. +// +// identityToken +// On success, receives a pointer to a null-terminated ANSI string +// containing the identity token. +// +// The string is allocated using CoTaskMemAlloc. The caller takes +// ownership of the returned memory and must free it by calling +// CoTaskMemFree when it is no longer needed. +// +// Return Value: +// S_OK on success. Otherwise, an HRESULT error code indicating the failure. +STDAPI WslcSessionAuthenticate( + _In_ WslcSession session, + _In_z_ PCSTR serverAddress, + _In_z_ PCSTR username, + _In_z_ PCSTR password, + _Outptr_result_z_ PSTR* identityToken, + _Outptr_opt_result_z_ PWSTR* errorMessage); + // Retrieves the list of container images // Parameters: // session diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index af4e0ee91..010ab0bb1 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -15,7 +15,10 @@ Module Name: #include "precomp.h" #include "Common.h" #include "wslcsdk.h" +#include "WslcsdkPrivate.h" +#include "WSLCContainerLauncher.h" #include "wslc_schema.h" +#include "WslcCredentialStore.h" #include extern std::wstring g_testDataPath; @@ -201,7 +204,7 @@ class WslcSdkTests VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &m_defaultSession, nullptr)); // Pull images required by the tests (no-op if already present). - for (const char* image : {"debian:latest", "python:3.12-alpine"}) + for (const char* image : {"debian:latest", "python:3.12-alpine", "hello-world:latest", "wslc-registry:latest"}) { LoadTestImage(image); } @@ -325,66 +328,6 @@ class WslcSdkTests VERIFY_ARE_EQUAL(future.get(), WSLC_SESSION_TERMINATION_REASON_SHUTDOWN); } - // ----------------------------------------------------------------------- - // Image tests - // ----------------------------------------------------------------------- - - TEST_METHOD(PullImage) - { - WSL2_TEST_ONLY(); - - // Positive: pull a well-known image. - { - WslcPullImageOptions opts{}; - opts.uri = "hello-world:linux"; - wil::unique_cotaskmem_string errorMsg; - HRESULT pullResult = WslcPullSessionImage(m_defaultSession, &opts, &errorMsg); - - // Skip test if error is due to rate limit. - if (pullResult == E_FAIL) - { - if (errorMsg) - { - if (wcsstr(errorMsg.get(), L"toomanyrequests") != nullptr) - { - LogWarning("Skipping PullImage test due to rate limiting."); - return; - } - } - } - - VERIFY_SUCCEEDED(pullResult); - - // Verify the image is usable by running a container from it. - auto output = RunContainerAndCapture(m_defaultSession, "hello-world:linux", {}); - VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos); - } - - // Negative: pull an image that does not exist. - { - WslcPullImageOptions opts{}; - opts.uri = "does-not:exist"; - wil::unique_cotaskmem_string errorMsg; - VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), WSLC_E_IMAGE_NOT_FOUND); - - // An error message should be present. - VERIFY_IS_NOT_NULL(errorMsg.get()); - } - - // Negative: null options pointer must fail. - { - wil::unique_cotaskmem_string errorMsg; - VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, nullptr, &errorMsg), E_POINTER); - } - - // Negative: null URI inside options must fail. - { - WslcPullImageOptions opts{}; - opts.uri = nullptr; - VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); - } - } - TEST_METHOD(ImageList) { WSL2_TEST_ONLY(); @@ -584,34 +527,17 @@ class WslcSdkTests { WSL2_TEST_ONLY(); - auto checkForImage = [this](std::string_view image) -> bool { - WslcImageInfo* images = nullptr; - uint32_t count = 0; - VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, &images, &count)); - auto cleanupImages = wil::scope_exit([images]() { CoTaskMemFree(images); }); - bool found = false; - for (uint32_t i = 0; i < count; ++i) - { - if (images[i].name == image) - { - found = true; - break; - } - } - return found; - }; - // Setup: load hello-world:latest so we have something to delete. LoadTestImage("hello-world:latest"); - VERIFY_IS_TRUE(checkForImage("hello-world:latest")); + VERIFY_IS_TRUE(HasImage("hello-world:latest")); // Positive: delete an existing image. wil::unique_cotaskmem_string errorMsg; VERIFY_SUCCEEDED(WslcDeleteSessionImage(m_defaultSession, "hello-world:latest", &errorMsg)); // Verify the image is no longer present in the list. - VERIFY_IS_FALSE(checkForImage("hello-world:latest")); + VERIFY_IS_FALSE(HasImage("hello-world:latest")); // Negative: null name must fail. VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER); @@ -2053,6 +1979,275 @@ class WslcSdkTests } } + // ----------------------------------------------------------------------- + // Authentication helpers + // ----------------------------------------------------------------------- + + // Starts a local registry container with host-mode networking and returns [container, registryAddress]. + // Uses the COM API (via GetInternalType) with WSLCContainerLauncher to get host-mode networking, + // which the SDK doesn't expose. Host networking shares the VM's network namespace, so the registry + // is reachable at 127.0.0.1: from both dockerd (inside the VM) and the host. + std::pair StartLocalRegistry(const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000) + { + std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; + if (!username.empty()) + { + env.push_back(std::format("USERNAME={}", username)); + env.push_back(std::format("PASSWORD={}", password)); + } + + wsl::windows::common::WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env); + launcher.SetEntrypoint({"/entrypoint.sh"}); + launcher.AddPort(port, port, AF_INET); + + // Get the IWSLCSession COM object from the SDK session handle. + // GetInternalType is not exported from the SDK DLL, so cast the opaque handle directly. + auto& session = *reinterpret_cast(m_defaultSession)->session; + auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); + + auto registryAddress = std::format("127.0.0.1:{}", port); + + // Wait for the registry to be ready by probing from the host. + auto hostUrl = std::format(L"http://{}", registryAddress); + auto expectedHttpStatus = username.empty() ? 200 : 401; + ExpectHttpResponse(hostUrl.c_str(), expectedHttpStatus, true); + + return {std::move(container), registryAddress}; + } + + // Tags and pushes an image to a local registry via the SDK APIs. + void PushImageToRegistry(const std::string& repo, const std::string& tag, const std::string& registryAddress, const std::string& registryAuth) + { + auto imageName = std::format("{}:{}", repo, tag); + auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag); + auto registryRepo = std::format("{}/{}", registryAddress, repo); + + // Tag the image with the registry address so it can be pushed. + WslcTagImageOptions tagOptions{}; + tagOptions.image = imageName.c_str(); + tagOptions.repo = registryRepo.c_str(); + tagOptions.tag = tag.c_str(); + VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &tagOptions, nullptr)); + + // Ensures the registry-prefixed tag is removed after the push. + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { + LOG_IF_FAILED(WslcDeleteSessionImage(m_defaultSession, registryImage.c_str(), nullptr)); + }); + + WslcPushImageOptions pushOptions{}; + pushOptions.image = registryImage.c_str(); + pushOptions.registryAuth = registryAuth.c_str(); + VERIFY_SUCCEEDED(WslcPushSessionImage(m_defaultSession, &pushOptions, nullptr)); + } + + bool HasImage(const std::string& imageName) + { + wil::unique_cotaskmem_array_ptr images; + VERIFY_SUCCEEDED(WslcListSessionImages(m_defaultSession, images.addressof(), images.size_address())); + + for (const auto& image : images) + { + if (image.name == imageName) + { + return true; + } + } + return false; + } + + // ----------------------------------------------------------------------- + // Authentication tests + // ----------------------------------------------------------------------- + + TEST_METHOD(AuthenticateTests) + { + WSL2_TEST_ONLY(); + + constexpr auto c_username = "wslctest"; + constexpr auto c_password = "password"; + + auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password); + + // Negative: wrong password must fail. + { + wil::unique_cotaskmem_ansistring token; + wil::unique_cotaskmem_string errorMsg; + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &errorMsg), E_FAIL); + VERIFY_IS_NOT_NULL(errorMsg.get()); + LogInfo("Authenticate error: %ws", errorMsg.get()); + } + + // Positive: correct credentials must succeed and return a non-null token. + { + wil::unique_cotaskmem_ansistring token; + wil::unique_cotaskmem_string errorMsg; + HRESULT hr = WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, &token, &errorMsg); + if (FAILED(hr)) + { + LogError("WslcSessionAuthenticate failed: 0x%08x, error: %ws", hr, errorMsg ? errorMsg.get() : L"(null)"); + } + VERIFY_SUCCEEDED(hr); + VERIFY_IS_NOT_NULL(token.get()); + } + + // Build the auth header for push/pull operations. + auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); + + // Push hello-world:latest to the authenticated registry. + PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); + + auto image = std::format("{}/hello-world:latest", registryAddress); + + // Pulling without credentials should fail. + { + WslcPullImageOptions opts{}; + opts.uri = image.c_str(); + wil::unique_cotaskmem_string errorMsg; + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL); + VERIFY_IS_NOT_NULL(errorMsg.get()); + LogInfo("Pull without credentials error: %ws", errorMsg.get()); + } + + // Pulling with credentials should succeed. + { + WslcPullImageOptions opts{}; + opts.uri = image.c_str(); + opts.registryAuth = xRegistryAuth.c_str(); + VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr)); + VERIFY_IS_TRUE(HasImage(image)); + } + + // Negative: null parameters must fail. + { + wil::unique_cotaskmem_ansistring token; + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, nullptr, c_username, c_password, &token, nullptr), E_POINTER); + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), nullptr, c_password, &token, nullptr), E_POINTER); + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, nullptr, &token, nullptr), E_POINTER); + VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, nullptr, nullptr), E_POINTER); + } + } + + TEST_METHOD(PullImage) + { + WSL2_TEST_ONLY(); + + { + // Start a local registry without auth to avoid Docker Hub rate limits. + auto [registryContainer, registryAddress] = StartLocalRegistry(); + + auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); + + // Push hello-world:latest to the local registry. + PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); + + auto image = std::format("{}/hello-world:latest", registryAddress); + + // Delete the image locally so the pull is a real pull. + WslcDeleteSessionImage(m_defaultSession, image.c_str(), nullptr); + + // Pull from the local registry. + { + WslcPullImageOptions opts{}; + opts.uri = image.c_str(); + VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr)); + } + + // Verify the pulled image is in the image list. + VERIFY_IS_TRUE(HasImage(image)); + + // Verify the image is usable by running a container from it. + auto output = RunContainerAndCapture(m_defaultSession, image.c_str(), {}); + VERIFY_IS_TRUE(output.stdoutOutput.find("Hello from Docker!") != std::string::npos); + } + + // Negative: pull an image that does not exist. + { + WslcPullImageOptions opts{}; + opts.uri = "does-not:exist"; + wil::unique_cotaskmem_string errorMsg; + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), WSLC_E_IMAGE_NOT_FOUND); + + // An error message should be present. + VERIFY_IS_NOT_NULL(errorMsg.get()); + } + + // Negative: null URI inside options must fail. + { + WslcPullImageOptions opts{}; + opts.uri = nullptr; + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); + } + } + + TEST_METHOD(PushImage) + { + WSL2_TEST_ONLY(); + + // Negative: pushing a non-existent image must fail. + { + WslcPushImageOptions opts{}; + opts.image = "does-not-exist:latest"; + wil::unique_cotaskmem_string errorMsg; + VERIFY_FAILED(WslcPushSessionImage(m_defaultSession, &opts, &errorMsg)); + } + + // Negative: null options must fail. + VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER); + + // Negative: null image inside options must fail. + { + WslcPushImageOptions opts{}; + opts.image = nullptr; + VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); + } + } + + TEST_METHOD(TagImage) + { + WSL2_TEST_ONLY(); + + // Positive: tag an existing image. + { + WslcTagImageOptions opts{}; + opts.image = "debian:latest"; + opts.repo = "debian"; + opts.tag = "sdk-test-tag"; + VERIFY_SUCCEEDED(WslcTagSessionImage(m_defaultSession, &opts, nullptr)); + + // Verify the tag is present. + VERIFY_IS_TRUE(HasImage("debian:sdk-test-tag")); + + // Cleanup: delete the tag. + WslcDeleteSessionImage(m_defaultSession, "debian:sdk-test-tag", nullptr); + } + + // Negative: null options must fail. + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER); + + // Negative: null fields must fail. + { + WslcTagImageOptions opts{}; + opts.image = nullptr; + opts.repo = "debian"; + opts.tag = "test"; + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); + } + { + WslcTagImageOptions opts{}; + opts.image = "debian:latest"; + opts.repo = nullptr; + opts.tag = "test"; + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); + } + { + WslcTagImageOptions opts{}; + opts.image = "debian:latest"; + opts.repo = "debian"; + opts.tag = nullptr; + VERIFY_ARE_EQUAL(WslcTagSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); + } + } + // ----------------------------------------------------------------------- // Stub tests for unimplemented (E_NOTIMPL) functions. // Each of these confirms the current state of the SDK; once the underlying From adab0cebdb93ad6eaf2268d55f393655577c2d02 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 16:02:17 -0700 Subject: [PATCH 23/76] remove unneeded code --- src/windows/WslcSDK/WslcsdkPrivate.h | 2 -- src/windows/WslcSDK/wslcsdk.cpp | 22 ---------------------- src/windows/WslcSDK/wslcsdk.def | 1 - src/windows/WslcSDK/wslcsdk.h | 4 +--- src/windows/wslcsession/WSLCSession.cpp | 13 ------------- test/windows/WslcSdkTests.cpp | 6 ++++-- 6 files changed, 5 insertions(+), 43 deletions(-) diff --git a/src/windows/WslcSDK/WslcsdkPrivate.h b/src/windows/WslcSDK/WslcsdkPrivate.h index d5d38d58c..339d7f855 100644 --- a/src/windows/WslcSDK/WslcsdkPrivate.h +++ b/src/windows/WslcSDK/WslcsdkPrivate.h @@ -89,8 +89,6 @@ typedef struct WslcContainerOptionsInternal const WslcContainerProcessOptionsInternal* initProcessOptions; WSLCContainerNetworkType networking; WslcContainerFlags containerFlags; - PCSTR const* entrypoint; - uint32_t entrypointCount; } WslcContainerOptionsInternal; diff --git a/src/windows/WslcSDK/wslcsdk.cpp b/src/windows/WslcSDK/wslcsdk.cpp index 37db2b40c..ac2b00309 100644 --- a/src/windows/WslcSDK/wslcsdk.cpp +++ b/src/windows/WslcSDK/wslcsdk.cpp @@ -684,12 +684,6 @@ try containerOptions.ContainerNetwork.ContainerNetworkType = internalContainerSettings->networking; - if (internalContainerSettings->entrypoint && internalContainerSettings->entrypointCount) - { - containerOptions.Entrypoint.Values = internalContainerSettings->entrypoint; - containerOptions.Entrypoint.Count = internalContainerSettings->entrypointCount; - } - // TODO: No user access // containerOptions.Labels; // containerOptions.LabelsCount; @@ -750,22 +744,6 @@ try } CATCH_RETURN(); -STDAPI WslcSetContainerSettingsEntrypoint(_In_ WslcContainerSettings* containerSettings, _In_reads_(argc) PCSTR const* argv, _In_ size_t argc) -try -{ - auto internalType = CheckAndGetInternalType(containerSettings); - RETURN_HR_IF( - E_INVALIDARG, - (argv == nullptr && argc != 0) || (argv != nullptr && argc == 0) || - (argc > static_cast(std::numeric_limits::max()))); - - internalType->entrypoint = argv; - internalType->entrypointCount = static_cast(argc); - - return S_OK; -} -CATCH_RETURN(); - STDAPI WslcSetContainerSettingsName(_In_ WslcContainerSettings* containerSettings, _In_ PCSTR name) try { diff --git a/src/windows/WslcSDK/wslcsdk.def b/src/windows/WslcSDK/wslcsdk.def index e3acd91f5..800c57d5c 100644 --- a/src/windows/WslcSDK/wslcsdk.def +++ b/src/windows/WslcSDK/wslcsdk.def @@ -45,7 +45,6 @@ WslcSetContainerSettingsNamedVolumes WslcSetContainerSettingsInitProcess WslcSetContainerSettingsFlags WslcSetContainerSettingsPortMappings -WslcSetContainerSettingsEntrypoint WslcCreateContainerProcess WslcStartContainer diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index fe70320e9..b2a359d92 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -32,7 +32,7 @@ typedef struct WslcSessionSettings DECLARE_HANDLE(WslcSession); // Container values -#define WSLC_CONTAINER_OPTIONS_SIZE 112 +#define WSLC_CONTAINER_OPTIONS_SIZE 96 #define WSLC_CONTAINER_OPTIONS_ALIGNMENT 8 typedef struct WslcContainerSettings @@ -180,8 +180,6 @@ STDAPI WslcSetContainerSettingsDomainName(_In_ WslcContainerSettings* containerS STDAPI WslcSetContainerSettingsFlags(_In_ WslcContainerSettings* containerSettings, _In_ WslcContainerFlags flags); -STDAPI WslcSetContainerSettingsEntrypoint(_In_ WslcContainerSettings* containerSettings, _In_reads_(argc) PCSTR const* argv, _In_ size_t argc); - STDAPI WslcSetContainerSettingsPortMappings( _In_ WslcContainerSettings* containerSettings, _In_reads_opt_(portMappingCount) const WslcContainerPortMapping* portMappings, diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index 5a1e962a1..6982575e1 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -119,19 +119,6 @@ std::string IndentLines(const std::string& input, const std::string& prefix) return result; } -std::pair> ParseImage(const std::string& Input) -{ - size_t separator = Input.find_last_of(':'); - if (separator == std::string::npos) - { - return {Input, {}}; - } - - THROW_HR_WITH_USER_ERROR_IF(E_INVALIDARG, Localization::MessageWslcInvalidImage(Input), separator >= Input.size() - 1 || separator == 0); - - return {Input.substr(0, separator), Input.substr(separator + 1)}; -} - void ValidateName(LPCSTR Name) { const auto& locale = std::locale::classic(); diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index 010ab0bb1..c925b7dd2 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -1987,7 +1987,8 @@ class WslcSdkTests // Uses the COM API (via GetInternalType) with WSLCContainerLauncher to get host-mode networking, // which the SDK doesn't expose. Host networking shares the VM's network namespace, so the registry // is reachable at 127.0.0.1: from both dockerd (inside the VM) and the host. - std::pair StartLocalRegistry(const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000) + std::pair StartLocalRegistry( + const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000) { std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; if (!username.empty()) @@ -2072,7 +2073,8 @@ class WslcSdkTests { wil::unique_cotaskmem_ansistring token; wil::unique_cotaskmem_string errorMsg; - VERIFY_ARE_EQUAL(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &errorMsg), E_FAIL); + VERIFY_ARE_EQUAL( + WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); LogInfo("Authenticate error: %ws", errorMsg.get()); } From 3d0bad5f5fe2252559be7a9966175148afbbc5d8 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 16:07:54 -0700 Subject: [PATCH 24/76] Address custom headers suggestion --- src/windows/wslcsession/DockerHTTPClient.cpp | 28 ++++++++------------ src/windows/wslcsession/DockerHTTPClient.h | 5 ++-- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 45e469db5..d393c004a 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -139,7 +139,7 @@ std::unique_ptr DockerHTTPClient::PullImag customHeaders["X-Registry-Auth"] = registryAuth.value(); } - return SendRequestImpl(verb::post, url, {}, {}, customHeaders); + return SendRequestImpl(verb::post, url, {}, customHeaders); } std::unique_ptr DockerHTTPClient::LoadImage(uint64_t ContentLength) @@ -148,7 +148,7 @@ std::unique_ptr DockerHTTPClient::LoadImag verb::post, URL::Create("/images/load"), {}, - {{http::field::content_type, "application/x-tar"}, {http::field::content_length, std::to_string(ContentLength)}}); + {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); } std::unique_ptr DockerHTTPClient::ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength) @@ -159,7 +159,7 @@ std::unique_ptr DockerHTTPClient::ImportIm url.SetParameter("fromSrc", "-"); return SendRequestImpl( - verb::post, url, {}, {{http::field::content_type, "application/x-tar"}, {http::field::content_length, std::to_string(ContentLength)}}); + verb::post, url, {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); } void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag) @@ -182,7 +182,7 @@ std::unique_ptr DockerHTTPClient::PushImag } std::map customHeaders = {{"X-Registry-Auth", registryAuth}}; - return SendRequestImpl(verb::post, url, {}, {}, customHeaders); + return SendRequestImpl(verb::post, url, {}, customHeaders); } std::string DockerHTTPClient::Authenticate(const std::string& serverAddress, const std::string& username, const std::string& password) @@ -347,8 +347,8 @@ docker_schema::InspectExec DockerHTTPClient::InspectExec(const std::string& Id) wil::unique_socket DockerHTTPClient::AttachContainer(const std::string& Id, const std::optional& DetachKeys) { - std::map headers{ - {boost::beast::http::field::upgrade, "tcp"}, {boost::beast::http::field::connection, "upgrade"}}; + std::map headers{ + {"Upgrade", "tcp"}, {"Connection", "upgrade"}}; auto url = URL::Create("/containers/{}/attach", Id); url.SetParameter("stream", true); @@ -444,8 +444,8 @@ docker_schema::CreateExecResponse DockerHTTPClient::CreateExec(const std::string wil::unique_socket DockerHTTPClient::StartExec(const std::string& Id, const common::docker_schema::StartExec& Request) { - std::map headers{ - {boost::beast::http::field::upgrade, "tcp"}, {boost::beast::http::field::connection, "upgrade"}}; + std::map headers{ + {"Upgrade", "tcp"}, {"Connection", "upgrade"}}; auto url = URL::Create("/exec/{}/start", Id); @@ -665,8 +665,7 @@ std::unique_ptr DockerHTTPClient::SendRequ verb Method, const URL& Url, const std::string& Body, - const std::map& Headers, - const std::map& CustomHeaders) + const std::map& Headers) { auto context = std::make_unique(ConnectSocket()); @@ -684,12 +683,7 @@ std::unique_ptr DockerHTTPClient::SendRequ req.set(http::field::connection, "close"); req.set(http::field::accept, "application/json"); - for (const auto& [field, value] : Headers) - { - req.set(field, value); - } - - for (const auto& [name, value] : CustomHeaders) + for (const auto& [name, value] : Headers) { req.set(name, value); } @@ -711,7 +705,7 @@ std::unique_ptr DockerHTTPClient::SendRequ } std::pair DockerHTTPClient::SendRequest( - verb Method, const URL& Url, const std::string& Body, const std::map& Headers) + verb Method, const URL& Url, const std::string& Body, const std::map& Headers) { // Write the request auto context = SendRequestImpl(Method, Url, Body, Headers); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index f9480ccd5..904842f93 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -230,14 +230,13 @@ class DockerHTTPClient boost::beast::http::verb Method, const URL& Url, const std::string& Body, - const std::map& Headers = {}, - const std::map& CustomHeaders = {}); + const std::map& Headers = {}); std::pair SendRequestAndReadResponse( boost::beast::http::verb Method, const URL& Url, const std::string& Body = ""); std::pair SendRequest( - boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map& Headers = {}); + boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map& Headers = {}); template auto Transaction(boost::beast::http::verb Method, const URL& Url, const TRequest& RequestObject = {}) From 10491eee17a48ac44224e7543c54475ba0ec2371 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 16:11:51 -0700 Subject: [PATCH 25/76] Make regitsry auth required --- src/windows/WslcSDK/wslcsdk.cpp | 3 ++- src/windows/WslcSDK/wslcsdk.h | 2 +- tools/test/images/build-image.ps1 | 18 +++++++++++------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/windows/WslcSDK/wslcsdk.cpp b/src/windows/WslcSDK/wslcsdk.cpp index ac2b00309..6c2fa4566 100644 --- a/src/windows/WslcSDK/wslcsdk.cpp +++ b/src/windows/WslcSDK/wslcsdk.cpp @@ -1308,11 +1308,12 @@ try RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), internalType->session); RETURN_HR_IF_NULL(E_POINTER, options); RETURN_HR_IF_NULL(E_INVALIDARG, options->image); + RETURN_HR_IF_NULL(E_INVALIDARG, options->registryAuth); auto progressCallback = ProgressCallback::CreateIf(options); return errorInfoWrapper.CaptureResult( - internalType->session->PushImage(options->image, options->registryAuth ? options->registryAuth : "", progressCallback.get())); + internalType->session->PushImage(options->image, options->registryAuth, progressCallback.get())); } CATCH_RETURN(); diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index b2a359d92..c582e090a 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -459,7 +459,7 @@ STDAPI WslcTagSessionImage(_In_ WslcSession session, _In_ const WslcTagImageOpti typedef struct WslcPushImageOptions { _In_z_ PCSTR image; - _In_opt_z_ PCSTR registryAuth; // Base64-encoded X-Registry-Auth header value. + _In_z_ PCSTR registryAuth; // Base64-encoded X-Registry-Auth header value. _In_opt_ WslcContainerImageProgressCallback progressCallback; _In_opt_ PVOID progressCallbackContext; } WslcPushImageOptions; diff --git a/tools/test/images/build-image.ps1 b/tools/test/images/build-image.ps1 index b7d275798..50a876d80 100644 --- a/tools/test/images/build-image.ps1 +++ b/tools/test/images/build-image.ps1 @@ -4,12 +4,12 @@ .DESCRIPTION This script builds a custom image using wslc from a specified Dockerfile and saves the resulting image as a .tar file. This is useful for preparing test images for WSL container tests. -.PARAMETER Dockerfile - Path to the Dockerfile to build. +.PARAMETER DockerfileDir + Path to the directory containing the Dockerfile to build. .PARAMETER ImageTag Tag for the built image. .PARAMETER OutputFile - Path to save the exported .tar file. + Path to save the exported .tar file. Defaults to .tar in the current directory. #> [CmdletBinding(SupportsShouldProcess)] @@ -34,10 +34,14 @@ if ([System.IO.Path]::GetExtension($OutputFile) -ne ".tar") { } -& wslc build -t $ImageTag $DockerfileDir -if ($LASTEXITCODE -ne 0) { throw "wslc build failed with exit code $LASTEXITCODE" } +if ($PSCmdlet.ShouldProcess($ImageTag, "Build image from '$DockerfileDir'")) { + & wslc build -t $ImageTag $DockerfileDir + if ($LASTEXITCODE -ne 0) { throw "wslc build failed with exit code $LASTEXITCODE" } +} -& wslc save --output $OutputFile $ImageTag -if ($LASTEXITCODE -ne 0) { throw "wslc save failed with exit code $LASTEXITCODE" } +if ($PSCmdlet.ShouldProcess($OutputFile, "Save image '$ImageTag'")) { + & wslc save --output $OutputFile $ImageTag + if ($LASTEXITCODE -ne 0) { throw "wslc save failed with exit code $LASTEXITCODE" } +} Write-Host "Image built and saved to $OutputFile successfully." From 404a05e0628955906c892da0040b3b8ce8756785 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 16:13:42 -0700 Subject: [PATCH 26/76] Nit update script --- tools/test/images/build-image.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/test/images/build-image.ps1 b/tools/test/images/build-image.ps1 index 50a876d80..b9b11c7e4 100644 --- a/tools/test/images/build-image.ps1 +++ b/tools/test/images/build-image.ps1 @@ -42,6 +42,7 @@ if ($PSCmdlet.ShouldProcess($ImageTag, "Build image from '$DockerfileDir'")) { if ($PSCmdlet.ShouldProcess($OutputFile, "Save image '$ImageTag'")) { & wslc save --output $OutputFile $ImageTag if ($LASTEXITCODE -ne 0) { throw "wslc save failed with exit code $LASTEXITCODE" } + + Write-Host "Image built and saved to $OutputFile successfully." } -Write-Host "Image built and saved to $OutputFile successfully." From 2f35e77c2a59b2701cd9692303a18569cd6c7b33 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 18:55:04 -0700 Subject: [PATCH 27/76] Fix formatting --- src/windows/WslcSDK/wslcsdk.cpp | 3 +-- src/windows/wslcsession/DockerHTTPClient.cpp | 19 +++++-------------- src/windows/wslcsession/DockerHTTPClient.h | 5 +---- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/windows/WslcSDK/wslcsdk.cpp b/src/windows/WslcSDK/wslcsdk.cpp index 6c2fa4566..e56494b0d 100644 --- a/src/windows/WslcSDK/wslcsdk.cpp +++ b/src/windows/WslcSDK/wslcsdk.cpp @@ -1312,8 +1312,7 @@ try auto progressCallback = ProgressCallback::CreateIf(options); - return errorInfoWrapper.CaptureResult( - internalType->session->PushImage(options->image, options->registryAuth, progressCallback.get())); + return errorInfoWrapper.CaptureResult(internalType->session->PushImage(options->image, options->registryAuth, progressCallback.get())); } CATCH_RETURN(); diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index d393c004a..5c43f6bbc 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -145,10 +145,7 @@ std::unique_ptr DockerHTTPClient::PullImag std::unique_ptr DockerHTTPClient::LoadImage(uint64_t ContentLength) { return SendRequestImpl( - verb::post, - URL::Create("/images/load"), - {}, - {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); + verb::post, URL::Create("/images/load"), {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); } std::unique_ptr DockerHTTPClient::ImportImage(const std::string& Repo, const std::string& Tag, uint64_t ContentLength) @@ -158,8 +155,7 @@ std::unique_ptr DockerHTTPClient::ImportIm url.SetParameter("repo", Repo); url.SetParameter("fromSrc", "-"); - return SendRequestImpl( - verb::post, url, {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); + return SendRequestImpl(verb::post, url, {}, {{"Content-Type", "application/x-tar"}, {"Content-Length", std::to_string(ContentLength)}}); } void DockerHTTPClient::TagImage(const std::string& Id, const std::string& Repo, const std::string& Tag) @@ -347,8 +343,7 @@ docker_schema::InspectExec DockerHTTPClient::InspectExec(const std::string& Id) wil::unique_socket DockerHTTPClient::AttachContainer(const std::string& Id, const std::optional& DetachKeys) { - std::map headers{ - {"Upgrade", "tcp"}, {"Connection", "upgrade"}}; + std::map headers{{"Upgrade", "tcp"}, {"Connection", "upgrade"}}; auto url = URL::Create("/containers/{}/attach", Id); url.SetParameter("stream", true); @@ -444,8 +439,7 @@ docker_schema::CreateExecResponse DockerHTTPClient::CreateExec(const std::string wil::unique_socket DockerHTTPClient::StartExec(const std::string& Id, const common::docker_schema::StartExec& Request) { - std::map headers{ - {"Upgrade", "tcp"}, {"Connection", "upgrade"}}; + std::map headers{{"Upgrade", "tcp"}, {"Connection", "upgrade"}}; auto url = URL::Create("/exec/{}/start", Id); @@ -662,10 +656,7 @@ void DockerHTTPClient::DockerHttpResponseHandle::OnResponseBytes(const gsl::span } std::unique_ptr DockerHTTPClient::SendRequestImpl( - verb Method, - const URL& Url, - const std::string& Body, - const std::map& Headers) + verb Method, const URL& Url, const std::string& Body, const std::map& Headers) { auto context = std::make_unique(ConnectSocket()); diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index 904842f93..f6af40c7a 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -227,10 +227,7 @@ class DockerHTTPClient wil::unique_socket ConnectSocket(); std::unique_ptr SendRequestImpl( - boost::beast::http::verb Method, - const URL& Url, - const std::string& Body, - const std::map& Headers = {}); + boost::beast::http::verb Method, const URL& Url, const std::string& Body, const std::map& Headers = {}); std::pair SendRequestAndReadResponse( boost::beast::http::verb Method, const URL& Url, const std::string& Body = ""); From 71081bb0e58865e22d9b650a01d5c8005de05ec5 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 7 Apr 2026 18:58:04 -0700 Subject: [PATCH 28/76] Update comment --- test/windows/WslcSdkTests.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index c925b7dd2..bf7635f69 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -2002,7 +2002,6 @@ class WslcSdkTests launcher.AddPort(port, port, AF_INET); // Get the IWSLCSession COM object from the SDK session handle. - // GetInternalType is not exported from the SDK DLL, so cast the opaque handle directly. auto& session = *reinterpret_cast(m_defaultSession)->session; auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); From a40b27da8fc0fa2a93df96bb9fd6735e59fe1ea2 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 8 Apr 2026 09:19:36 -0700 Subject: [PATCH 29/76] Fix tests --- test/windows/WslcSdkTests.cpp | 72 ++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index bf7635f69..b5b1c45e7 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -2009,8 +2009,7 @@ class WslcSdkTests // Wait for the registry to be ready by probing from the host. auto hostUrl = std::format(L"http://{}", registryAddress); - auto expectedHttpStatus = username.empty() ? 200 : 401; - ExpectHttpResponse(hostUrl.c_str(), expectedHttpStatus, true); + ExpectHttpResponse(hostUrl.c_str(), 200, true); return {std::move(container), registryAddress}; } @@ -2075,47 +2074,51 @@ class WslcSdkTests VERIFY_ARE_EQUAL( WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, "wrong-password", &token, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); - LogInfo("Authenticate error: %ws", errorMsg.get()); } // Positive: correct credentials must succeed and return a non-null token. { wil::unique_cotaskmem_ansistring token; wil::unique_cotaskmem_string errorMsg; - HRESULT hr = WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, &token, &errorMsg); - if (FAILED(hr)) - { - LogError("WslcSessionAuthenticate failed: 0x%08x, error: %ws", hr, errorMsg ? errorMsg.get() : L"(null)"); - } - VERIFY_SUCCEEDED(hr); + VERIFY_SUCCEEDED(WslcSessionAuthenticate(m_defaultSession, registryAddress.c_str(), c_username, c_password, &token, &errorMsg)); VERIFY_IS_NOT_NULL(token.get()); } - // Build the auth header for push/pull operations. auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); - - // Push hello-world:latest to the authenticated registry. PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); auto image = std::format("{}/hello-world:latest", registryAddress); - // Pulling without credentials should fail. + // Pulling with credentials should succeed. { WslcPullImageOptions opts{}; opts.uri = image.c_str(); + opts.registryAuth = xRegistryAuth.c_str(); + VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr)); + VERIFY_IS_TRUE(HasImage(image)); + } + + // Negative: Pulling without credentials should fail. + { + WslcPullImageOptions opts{}; + opts.uri = image.c_str(); + wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL); VERIFY_IS_NOT_NULL(errorMsg.get()); - LogInfo("Pull without credentials error: %ws", errorMsg.get()); } - // Pulling with credentials should succeed. + // Negative: Pulling with bad credentials should fail. { + auto badAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, "wrong", registryAddress); + WslcPullImageOptions opts{}; opts.uri = image.c_str(); - opts.registryAuth = xRegistryAuth.c_str(); - VERIFY_SUCCEEDED(WslcPullSessionImage(m_defaultSession, &opts, nullptr)); - VERIFY_IS_TRUE(HasImage(image)); + opts.registryAuth = badAuth.c_str(); + + wil::unique_cotaskmem_string errorMsg; + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL); + VERIFY_IS_NOT_NULL(errorMsg.get()); } // Negative: null parameters must fail. @@ -2132,12 +2135,11 @@ class WslcSdkTests { WSL2_TEST_ONLY(); - { - // Start a local registry without auth to avoid Docker Hub rate limits. - auto [registryContainer, registryAddress] = StartLocalRegistry(); - - auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); + // Start a local registry without auth to avoid Docker Hub rate limits. + auto [registryContainer, registryAddress] = StartLocalRegistry(); + auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); + { // Push hello-world:latest to the local registry. PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); @@ -2163,20 +2165,23 @@ class WslcSdkTests // Negative: pull an image that does not exist. { + auto image = std::format("{}/does-not-exist", registryAddress); + WslcPullImageOptions opts{}; - opts.uri = "does-not:exist"; + opts.uri = image.c_str(); + opts.registryAuth = xRegistryAuth.c_str(); + wil::unique_cotaskmem_string errorMsg; VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), WSLC_E_IMAGE_NOT_FOUND); - - // An error message should be present. - VERIFY_IS_NOT_NULL(errorMsg.get()); } // Negative: null URI inside options must fail. { WslcPullImageOptions opts{}; opts.uri = nullptr; - VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); + + wil::unique_cotaskmem_string errorMsg; + VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &opts, &errorMsg), E_INVALIDARG); } } @@ -2184,12 +2189,17 @@ class WslcSdkTests { WSL2_TEST_ONLY(); + auto emptyRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader("", "", ""); + // Negative: pushing a non-existent image must fail. { WslcPushImageOptions opts{}; - opts.image = "does-not-exist:latest"; + opts.image = "does-not-exist"; + opts.registryAuth = emptyRegistryAuth.c_str(); + wil::unique_cotaskmem_string errorMsg; - VERIFY_FAILED(WslcPushSessionImage(m_defaultSession, &opts, &errorMsg)); + VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, &errorMsg), E_FAIL); + VERIFY_IS_NOT_NULL(errorMsg.get()); } // Negative: null options must fail. @@ -2199,6 +2209,8 @@ class WslcSdkTests { WslcPushImageOptions opts{}; opts.image = nullptr; + opts.registryAuth = emptyRegistryAuth.c_str(); + VERIFY_ARE_EQUAL(WslcPushSessionImage(m_defaultSession, &opts, nullptr), E_INVALIDARG); } } From ef6f1925b094901f7e5dccdc973a3e01a7a3a7ef Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 8 Apr 2026 11:12:37 -0700 Subject: [PATCH 30/76] Fix tests --- test/windows/WslcSdkTests.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index b5b1c45e7..05a087bda 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -527,9 +527,6 @@ class WslcSdkTests { WSL2_TEST_ONLY(); - // Setup: load hello-world:latest so we have something to delete. - LoadTestImage("hello-world:latest"); - VERIFY_IS_TRUE(HasImage("hello-world:latest")); // Positive: delete an existing image. @@ -539,6 +536,9 @@ class WslcSdkTests // Verify the image is no longer present in the list. VERIFY_IS_FALSE(HasImage("hello-world:latest")); + // Reload the image for subsequent tests. + LoadTestImage("hello-world:latest"); + // Negative: null name must fail. VERIFY_ARE_EQUAL(WslcDeleteSessionImage(m_defaultSession, nullptr, nullptr), E_POINTER); } @@ -1990,6 +1990,8 @@ class WslcSdkTests std::pair StartLocalRegistry( const std::string& username = {}, const std::string& password = {}, uint16_t port = 5000) { + VERIFY_IS_TRUE(HasImage("wslc-registry:latest")); + std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; if (!username.empty()) { @@ -2021,6 +2023,8 @@ class WslcSdkTests auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag); auto registryRepo = std::format("{}/{}", registryAddress, repo); + VERIFY_IS_TRUE(HasImage(imageName)); + // Tag the image with the registry address so it can be pushed. WslcTagImageOptions tagOptions{}; tagOptions.image = imageName.c_str(); From d14cfec5456031c5b4dbd4677f4286cec0e11b4e Mon Sep 17 00:00:00 2001 From: kvega005 Date: Thu, 9 Apr 2026 11:21:45 -0700 Subject: [PATCH 31/76] Added back removed test + cleanup --- src/windows/common/CMakeLists.txt | 2 - src/windows/common/WslcCredentialStore.cpp | 31 ----- src/windows/common/WslcCredentialStore.h | 30 ----- src/windows/common/wslutil.cpp | 14 +++ src/windows/common/wslutil.h | 8 ++ test/windows/WSLCTests.cpp | 135 +++++++++++++-------- test/windows/WslcSdkTests.cpp | 9 +- 7 files changed, 113 insertions(+), 116 deletions(-) delete mode 100644 src/windows/common/WslcCredentialStore.cpp delete mode 100644 src/windows/common/WslcCredentialStore.h diff --git a/src/windows/common/CMakeLists.txt b/src/windows/common/CMakeLists.txt index bb54a7e09..5693497d2 100644 --- a/src/windows/common/CMakeLists.txt +++ b/src/windows/common/CMakeLists.txt @@ -35,7 +35,6 @@ set(SOURCES SubProcess.cpp svccomm.cpp WSLCContainerLauncher.cpp - WslcCredentialStore.cpp VirtioNetworking.cpp WSLCProcessLauncher.cpp WslClient.cpp @@ -117,7 +116,6 @@ set(HEADERS SubProcess.h svccomm.hpp WSLCContainerLauncher.h - WslcCredentialStore.h VirtioNetworking.h WSLCProcessLauncher.h WslClient.h diff --git a/src/windows/common/WslcCredentialStore.cpp b/src/windows/common/WslcCredentialStore.cpp deleted file mode 100644 index e15266ec3..000000000 --- a/src/windows/common/WslcCredentialStore.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/*++ - -Copyright (c) Microsoft. All rights reserved. - -Module Name: - - WslcCredentialStore.cpp - -Abstract: - - Implementation of credential store helpers. - ---*/ - -#include "precomp.h" -#include "WslcCredentialStore.h" -#include "wslutil.h" - -std::string wsl::windows::common::BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress) -{ - nlohmann::json authJson = {{"username", username}, {"password", password}, {"serveraddress", serverAddress}}; - - return wslutil::Base64Encode(authJson.dump()); -} - -std::string wsl::windows::common::BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress) -{ - nlohmann::json authJson = {{"identitytoken", identityToken}, {"serveraddress", serverAddress}}; - - return wslutil::Base64Encode(authJson.dump()); -} diff --git a/src/windows/common/WslcCredentialStore.h b/src/windows/common/WslcCredentialStore.h deleted file mode 100644 index 987a210cc..000000000 --- a/src/windows/common/WslcCredentialStore.h +++ /dev/null @@ -1,30 +0,0 @@ -/*++ - -Copyright (c) Microsoft. All rights reserved. - -Module Name: - - WslcCredentialStore.h - -Abstract: - - Helpers for building Docker/OCI registry credential payloads. - ---*/ - -#pragma once -#include - -namespace wsl::windows::common { - -// Builds the base64-encoded X-Registry-Auth header value used by Docker APIs -// (PullImage, PushImage, etc.) from the given credentials. -std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress); - -// Builds the base64-encoded X-Registry-Auth header value from an identity token -// returned by Authenticate(). -std::string BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress); - -// TODO: Implement credential storage using WinCred - -} // namespace wsl::windows::common diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index b6d2e5f14..00250c165 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -1444,4 +1444,18 @@ std::string wsl::windows::common::wslutil::Base64Decode(const std::string& encod result.resize(size); return result; +} + +std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress) +{ + nlohmann::json authJson = {{"username", username}, {"password", password}, {"serveraddress", serverAddress}}; + + return Base64Encode(authJson.dump()); +} + +std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress) +{ + nlohmann::json authJson = {{"identitytoken", identityToken}, {"serveraddress", serverAddress}}; + + return Base64Encode(authJson.dump()); } \ No newline at end of file diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index 3318d8af1..81f8d2ca6 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -326,4 +326,12 @@ winrt::Windows::Management::Deployment::PackageVolume GetSystemVolume(); std::string Base64Encode(const std::string& input); std::string Base64Decode(const std::string& encoded); +// Builds the base64-encoded X-Registry-Auth header value used by Docker APIs +// (PullImage, PushImage, etc.) from the given credentials. +std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress); + +// Builds the base64-encoded X-Registry-Auth header value from an identity token +// returned by Authenticate(). +std::string BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress); + } // namespace wsl::windows::common::wslutil diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 23ea00664..b55c79742 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -17,7 +17,6 @@ Module Name: #include "wslc.h" #include "WSLCProcessLauncher.h" #include "WSLCContainerLauncher.h" -#include "WslcCredentialStore.h" #include "WslCoreFilesystem.h" using namespace std::literals::chrono_literals; @@ -197,12 +196,17 @@ class WSLCTests launcher.AddPort(port, port, AF_INET); auto container = launcher.Launch(*m_defaultSession, WSLCContainerStartFlagsNone); - return {std::move(container), std::format("127.0.0.1:{}", port)}; + + auto registryAddress = std::format("127.0.0.1:{}", port); + auto registryUrl = std::format(L"http://{}", registryAddress); + ExpectHttpResponse(registryUrl.c_str(), 200, true); + + return {std::move(container), std::move(registryAddress)}; } - static void PushImageToRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth) + std::string PushImageToRegistry(const std::string& imageName, const std::string& registryAddress, const std::string& registryAuth) { - auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); + auto [repo, tag] = ParseImage(imageName); auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest")); auto registryRepo = std::format("{}/{}", registryAddress, repo); auto registryTag = tag.value_or("latest"); @@ -213,16 +217,16 @@ class WSLCTests tagOptions.Tag = registryTag.c_str(); // Tag the image with the registry address so it can be pushed. - VERIFY_SUCCEEDED(session.TagImage(&tagOptions)); + VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions)); // Ensures the tag is removed to allow tests to try to push or pull the same image again. auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - WSLCDeleteImageOptions deleteOptions{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsNone}; - wil::unique_cotaskmem_array_ptr deletedImages; - LOG_IF_FAILED(session.DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address())); + LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsNone).first); }); - VERIFY_SUCCEEDED(session.PushImage(registryImage.c_str(), registryAuth.c_str(), nullptr)); + VERIFY_SUCCEEDED(m_defaultSession->PushImage(registryImage.c_str(), registryAuth.c_str(), nullptr)); + + return registryImage; } TEST_METHOD(GetVersion) @@ -477,21 +481,11 @@ class WSLCTests // Start a local registry without auth and push hello-world:latest to it. auto [registryContainer, registryAddress] = StartLocalRegistry(); - // Wait for the registry to be ready. - auto registryUrl = std::format(L"http://{}/v2/", registryAddress); - ExpectHttpResponse(registryUrl.c_str(), 200, true); - - PushImageToRegistry( - *m_defaultSession, "hello-world:latest", registryAddress, wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress)); - - auto image = std::format("{}/hello-world:latest", registryAddress); - - // Delete the image if it already exists locally, so the pull is a real pull. - WSLCDeleteImageOptions deleteOptions{.Image = image.c_str(), .Flags = WSLCDeleteImageFlagsForce}; - wil::unique_cotaskmem_array_ptr deletedImages; - LOG_IF_FAILED(m_defaultSession->DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address())); + auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", "", registryAddress)); + ExpectImagePresent(*m_defaultSession, image.c_str(), false); VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr)); + auto cleanup = wil::scope_exit([&]() { LOG_IF_FAILED(DeleteImageNoThrow(image, WSLCDeleteImageFlagsForce).first); }); // Verify that the image is in the list of images. ExpectImagePresent(*m_defaultSession, image.c_str()); @@ -531,22 +525,12 @@ class WSLCTests // Start a local registry without auth to avoid Docker Hub rate limits. auto [registryContainer, registryAddress] = StartLocalRegistry(); - auto auth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); - - // Wait for the registry to be ready. - auto registryUrl = std::format(L"http://{}/v2/", registryAddress); - ExpectHttpResponse(registryUrl.c_str(), 200, true); + auto auth = BuildRegistryAuthHeader("", "", registryAddress); - auto validatePull = [&](const std::string& sourceImage, const std::string& registryTag) { + auto validatePull = [&](const std::string& sourceImage) { // Push the source image to the local registry. - PushImageToRegistry(*m_defaultSession, sourceImage, registryAddress, auth); - - auto registryImage = std::format("{}/{}", registryAddress, registryTag); - - // Delete the image locally so the pull is a real pull. - WSLCDeleteImageOptions deleteOptions{.Image = registryImage.c_str(), .Flags = WSLCDeleteImageFlagsForce}; - wil::unique_cotaskmem_array_ptr deletedImages; - LOG_IF_FAILED(m_defaultSession->DeleteImage(&deleteOptions, &deletedImages, deletedImages.size_address())); + auto registryImage = PushImageToRegistry(sourceImage, registryAddress, auth); + ExpectImagePresent(*m_defaultSession, registryImage.c_str(), false); VERIFY_SUCCEEDED(m_defaultSession->PullImage(registryImage.c_str(), nullptr, nullptr)); @@ -556,9 +540,70 @@ class WSLCTests ExpectImagePresent(*m_defaultSession, registryImage.c_str()); }; - validatePull("debian:latest", "debian:latest"); - validatePull("alpine:latest", "alpine:latest"); - validatePull("hello-world:latest", "hello-world:latest"); + validatePull("debian:latest"); + validatePull("alpine:latest"); + validatePull("hello-world:latest"); + } + + TEST_METHOD(PullImageFromDockerHub) + { + WSL2_TEST_ONLY(); + SKIP_TEST_UNSTABLE(); + + auto validatePull = [&](const std::string& Image, const std::optional& ExpectedTag = {}) { + VERIFY_SUCCEEDED(m_defaultSession->PullImage(Image.c_str(), nullptr, nullptr)); + + auto cleanup = wil::scope_exit( + [&]() { LOG_IF_FAILED(DeleteImageNoThrow(ExpectedTag.value_or(Image), WSLCDeleteImageFlagsForce).first); }); + + if (!ExpectedTag.has_value()) + { + wil::unique_cotaskmem_array_ptr images; + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address())); + + for (const auto& e : images) + { + wil::unique_cotaskmem_ansistring json; + VERIFY_SUCCEEDED(m_defaultSession->InspectImage(e.Hash, &json)); + + auto parsed = wsl::shared::FromJson(json.get()); + + for (const auto& repoTag : parsed.RepoDigests.value_or({})) + { + if (Image == repoTag) + { + return; + } + } + } + + LogError("Expected digest '%hs' not found ", Image.c_str()); + + VERIFY_FAIL(); + } + else + { + ExpectImagePresent(*m_defaultSession, ExpectedTag->c_str()); + } + }; + + validatePull("ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30", {}); + validatePull("ubuntu", "ubuntu:latest"); + validatePull("debian:bookworm", "debian:bookworm"); + validatePull("pytorch/pytorch", "pytorch/pytorch:latest"); + validatePull("registry.k8s.io/pause:3.2", "registry.k8s.io/pause:3.2"); + + // Validate that PullImage() fails appropriately when the session runs out of space. + { + auto settings = GetDefaultSessionSettings(L"wslc-pull-image-out-of-space", false); + settings.NetworkingMode = WSLCNetworkingModeVirtioProxy; + settings.MemoryMb = 1024; + auto session = CreateSession(settings); + + VERIFY_ARE_EQUAL(session->PullImage("pytorch/pytorch", nullptr, nullptr), E_FAIL); + + ValidateCOMErrorMessageContains(L"no space left on device"); + } } TEST_METHOD(PushImage) @@ -580,7 +625,7 @@ class WSLCTests } } - TEST_METHOD(AuthenticateTests) + TEST_METHOD(Authenticate) { WSL2_TEST_ONLY(); @@ -589,11 +634,6 @@ class WSLCTests auto [registryContainer, registryAddress] = StartLocalRegistry(c_username, c_password); - auto registryUrl = std::format(L"http://{}/v2/", registryAddress); - - // The registry may take some time before it's up and running. Retry until it's ready to accept connections. - ExpectHttpResponse(registryUrl.c_str(), 401, true); - wil::unique_cotaskmem_ansistring token; VERIFY_ARE_EQUAL(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, "wrong-password", &token), E_FAIL); ValidateCOMErrorMessageContains(L"failed with status: 401 Unauthorized"); @@ -601,11 +641,10 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, c_password, &token)); VERIFY_IS_NOT_NULL(token.get()); - auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); - PushImageToRegistry(*m_defaultSession, "hello-world:latest", registryAddress, xRegistryAuth); + auto xRegistryAuth = BuildRegistryAuthHeader(c_username, c_password, registryAddress); + auto image = PushImageToRegistry("hello-world:latest", registryAddress, xRegistryAuth); // Pulling without credentials should fail. - auto image = std::format("{}/hello-world:latest", registryAddress); VERIFY_ARE_EQUAL(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr), E_FAIL); ValidateCOMErrorMessageContains(L"no basic auth credentials"); diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index 05a087bda..b207e0fd8 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -18,7 +18,6 @@ Module Name: #include "WslcsdkPrivate.h" #include "WSLCContainerLauncher.h" #include "wslc_schema.h" -#include "WslcCredentialStore.h" #include extern std::wstring g_testDataPath; @@ -2088,7 +2087,7 @@ class WslcSdkTests VERIFY_IS_NOT_NULL(token.get()); } - auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, c_password, registryAddress); + auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password, registryAddress); PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); auto image = std::format("{}/hello-world:latest", registryAddress); @@ -2114,7 +2113,7 @@ class WslcSdkTests // Negative: Pulling with bad credentials should fail. { - auto badAuth = wsl::windows::common::BuildRegistryAuthHeader(c_username, "wrong", registryAddress); + auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong", registryAddress); WslcPullImageOptions opts{}; opts.uri = image.c_str(); @@ -2141,7 +2140,7 @@ class WslcSdkTests // Start a local registry without auth to avoid Docker Hub rate limits. auto [registryContainer, registryAddress] = StartLocalRegistry(); - auto xRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader("", "", registryAddress); + auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "", registryAddress); { // Push hello-world:latest to the local registry. @@ -2193,7 +2192,7 @@ class WslcSdkTests { WSL2_TEST_ONLY(); - auto emptyRegistryAuth = wsl::windows::common::BuildRegistryAuthHeader("", "", ""); + auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "", ""); // Negative: pushing a non-existent image must fail. { From c9455170f89e6872c71a282255353926bd16919f Mon Sep 17 00:00:00 2001 From: kvega005 Date: Thu, 9 Apr 2026 11:51:35 -0700 Subject: [PATCH 32/76] Fix empty auth --- test/windows/WSLCTests.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index c69b54862..6a43532ed 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -595,7 +595,15 @@ class WSLCTests WSLC_TEST_METHOD(PushImage) { + auto emptyAuth = BuildRegistryAuthHeader("", "", ""); + // Validate that pushing a non-existent image fails. + { + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", emptyAuth.c_str(), nullptr), E_FAIL); + ValidateCOMErrorMessage(L"An image does not exist locally with the tag: does-not-exist"); + } + + // Validate passing empty auth string returns an appropriate error. { VERIFY_ARE_EQUAL(m_defaultSession->PushImage("does-not-exist:latest", "", nullptr), E_INVALIDARG); } @@ -603,10 +611,9 @@ class WSLCTests // Validate that PushImage() returns the appropriate error if the session is terminated. { VERIFY_SUCCEEDED(m_defaultSession->Terminate()); - auto cleanup = wil::scope_exit([&]() { ResetTestSession(); }); - VERIFY_ARE_EQUAL(m_defaultSession->PushImage("hello-world:latest", "", nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); + VERIFY_ARE_EQUAL(m_defaultSession->PushImage("hello-world:latest", emptyAuth.c_str(), nullptr), HRESULT_FROM_WIN32(ERROR_INVALID_STATE)); } } From baabcd9d5363657b07318cf55b3774a88f9d563e Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Thu, 9 Apr 2026 14:25:44 -0700 Subject: [PATCH 33/76] Fix clang-format violations in test files Apply clang-format (VS 2022 / v19.1.5) to fix formatting check failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/windows/WSLCTests.cpp | 5 ++--- test/windows/WslcSdkTests.cpp | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 6a43532ed..4514ce527 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -220,9 +220,8 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->TagImage(&tagOptions)); // Ensures the tag is removed to allow tests to try to push or pull the same image again. - auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { - LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsNone).first); - }); + auto cleanup = wil::scope_exit_log( + WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_FAILED(DeleteImageNoThrow(registryImage, WSLCDeleteImageFlagsNone).first); }); VERIFY_SUCCEEDED(m_defaultSession->PushImage(registryImage.c_str(), registryAuth.c_str(), nullptr)); diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index 1391110b9..7a9f01de7 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -324,7 +324,7 @@ class WslcSdkTests // ----------------------------------------------------------------------- // Image tests // ----------------------------------------------------------------------- - + WSLC_TEST_METHOD(ImageList) { // Positive: session has images pre-loaded — list must return at least one entry. From e944f0cdb8a1b093f4209626c037f2a0a7d1a550 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Fri, 10 Apr 2026 16:42:48 -0700 Subject: [PATCH 34/76] implement wslc login --- localization/strings/en-US/Resources.resw | 47 +++ src/windows/wslc/CMakeLists.txt | 4 +- .../wslc/arguments/ArgumentDefinitions.h | 4 + src/windows/wslc/commands/ImageCommand.cpp | 1 + src/windows/wslc/commands/ImageCommand.h | 15 + .../wslc/commands/ImagePushCommand.cpp | 51 +++ src/windows/wslc/commands/RegistryCommand.cpp | 167 +++++++++ src/windows/wslc/commands/RegistryCommand.h | 70 ++++ src/windows/wslc/commands/RootCommand.cpp | 5 + src/windows/wslc/services/ImageService.cpp | 35 +- src/windows/wslc/services/ImageService.h | 6 +- src/windows/wslc/services/RegistryService.cpp | 347 ++++++++++++++++++ src/windows/wslc/services/RegistryService.h | 62 ++++ src/windows/wslc/settings/UserSettings.cpp | 19 +- src/windows/wslc/settings/UserSettings.h | 8 + src/windows/wslc/tasks/ImageTasks.cpp | 11 + src/windows/wslc/tasks/ImageTasks.h | 1 + src/windows/wslc/tasks/RegistryTasks.cpp | 70 ++++ src/windows/wslc/tasks/RegistryTasks.h | 22 ++ test/windows/WSLCTests.cpp | 3 +- test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp | 4 + test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 58 +++ test/windows/wslc/e2e/WSLCE2EHelpers.h | 13 + test/windows/wslc/e2e/WSLCE2EImageTests.cpp | 1 + .../windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 196 ++++++++++ .../windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 216 +++++++++++ 26 files changed, 1429 insertions(+), 7 deletions(-) create mode 100644 src/windows/wslc/commands/ImagePushCommand.cpp create mode 100644 src/windows/wslc/commands/RegistryCommand.cpp create mode 100644 src/windows/wslc/commands/RegistryCommand.h create mode 100644 src/windows/wslc/services/RegistryService.cpp create mode 100644 src/windows/wslc/services/RegistryService.h create mode 100644 src/windows/wslc/tasks/RegistryTasks.cpp create mode 100644 src/windows/wslc/tasks/RegistryTasks.h create mode 100644 test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp create mode 100644 test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 462c7b36a..0dcb782b1 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2247,6 +2247,12 @@ For privacy information about this product please visit https://aka.ms/privacy.< Pulls images. + + Upload an image to a registry. + + + Upload an image to a registry. + Remove images. @@ -2259,6 +2265,47 @@ For privacy information about this product please visit https://aka.ms/privacy.< Saves images. + + Log in to a registry. + + + Log in to a registry. If no server is specified, the default is defined by the session. + + + Log out from a registry. + + + Log out from a registry. If no server is specified, the default is defined by the session. + + + Login Succeeded + + + Removing login credentials for {} + + + Not logged in to {} + + + Server + + + Username + + + Password or Personal Access Token (PAT) + {Locked="PAT"}Acronym should not be translated + + + Take the Password or Personal Access Token (PAT) from stdin + {Locked="PAT"}{Locked="stdin"}Technical terms should not be translated + + + Manage registry credentials. + + + Manage registry credentials, including logging in and out of container registries. + Manage sessions. diff --git a/src/windows/wslc/CMakeLists.txt b/src/windows/wslc/CMakeLists.txt index c160840c5..ba00467ef 100644 --- a/src/windows/wslc/CMakeLists.txt +++ b/src/windows/wslc/CMakeLists.txt @@ -14,7 +14,9 @@ target_include_directories(wslclib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${WSLC_SUB target_link_libraries(wslclib ${COMMON_LINK_LIBRARIES} common - yaml-cpp) + yaml-cpp + advapi32 + crypt32) target_precompile_headers(wslclib REUSE_FROM common) set_target_properties(wslclib PROPERTIES FOLDER windows) diff --git a/src/windows/wslc/arguments/ArgumentDefinitions.h b/src/windows/wslc/arguments/ArgumentDefinitions.h index 3748a95cd..7fbf8e1cb 100644 --- a/src/windows/wslc/arguments/ArgumentDefinitions.h +++ b/src/windows/wslc/arguments/ArgumentDefinitions.h @@ -63,6 +63,8 @@ _(Name, "name", NO_ALIAS, Kind::Value, L _(NoPrune, "no-prune", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoPruneArgDescription()) \ _(NoTrunc, "no-trunc", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_NoTruncArgDescription()) \ _(Output, "output", L"o", Kind::Value, Localization::WSLCCLI_OutputArgDescription()) \ +_(Password, "password", L"p", Kind::Value, Localization::WSLCCLI_LoginPasswordArgDescription()) \ +_(PasswordStdin, "password-stdin", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_LoginPasswordStdinArgDescription()) \ _(Path, "path", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_PathArgDescription()) \ /*_(Progress, "progress", NO_ALIAS, Kind::Value, Localization::WSLCCLI_ProgressArgDescription())*/ \ _(Publish, "publish", L"p", Kind::Value, Localization::WSLCCLI_PublishArgDescription()) \ @@ -70,6 +72,7 @@ _(Publish, "publish", L"p", Kind::Value, L _(Quiet, "quiet", L"q", Kind::Flag, Localization::WSLCCLI_QuietArgDescription()) \ _(Remove, "rm", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_RemoveArgDescription()) \ /*_(Scheme, "scheme", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SchemeArgDescription())*/ \ +_(Server, "server", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_LoginServerArgDescription()) \ _(Session, "session", NO_ALIAS, Kind::Value, Localization::WSLCCLI_SessionIdArgDescription()) \ _(SessionId, "session-id", NO_ALIAS, Kind::Positional, Localization::WSLCCLI_SessionIdPositionalArgDescription()) \ _(StoragePath, "storage-path", NO_ALIAS, Kind::Positional, L"Path to the session storage directory") \ @@ -79,6 +82,7 @@ _(Time, "time", L"t", Kind::Value, L _(TMPFS, "tmpfs", NO_ALIAS, Kind::Value, Localization::WSLCCLI_TMPFSArgDescription()) \ _(TTY, "tty", L"t", Kind::Flag, Localization::WSLCCLI_TTYArgDescription()) \ _(User, "user", L"u", Kind::Value, Localization::WSLCCLI_UserArgDescription()) \ +_(Username, "username", L"u", Kind::Value, Localization::WSLCCLI_LoginUsernameArgDescription()) \ _(Verbose, "verbose", NO_ALIAS, Kind::Flag, Localization::WSLCCLI_VerboseArgDescription()) \ _(Version, "version", L"v", Kind::Flag, Localization::WSLCCLI_VersionArgDescription()) \ /*_(Virtual, "virtualization", NO_ALIAS, Kind::Value, Localization::WSLCCLI_VirtualArgDescription())*/ \ diff --git a/src/windows/wslc/commands/ImageCommand.cpp b/src/windows/wslc/commands/ImageCommand.cpp index 313d59ae0..eb09c3833 100644 --- a/src/windows/wslc/commands/ImageCommand.cpp +++ b/src/windows/wslc/commands/ImageCommand.cpp @@ -28,6 +28,7 @@ std::vector> ImageCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); return commands; } diff --git a/src/windows/wslc/commands/ImageCommand.h b/src/windows/wslc/commands/ImageCommand.h index aea33fc44..21034c151 100644 --- a/src/windows/wslc/commands/ImageCommand.h +++ b/src/windows/wslc/commands/ImageCommand.h @@ -146,6 +146,21 @@ struct ImagePullCommand final : public Command void ExecuteInternal(CLIExecutionContext& context) const override; }; +// Push Command +struct ImagePushCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"push"; + ImagePushCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + // Save Command struct ImageSaveCommand final : public Command { diff --git a/src/windows/wslc/commands/ImagePushCommand.cpp b/src/windows/wslc/commands/ImagePushCommand.cpp new file mode 100644 index 000000000..8db9d28cf --- /dev/null +++ b/src/windows/wslc/commands/ImagePushCommand.cpp @@ -0,0 +1,51 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ImagePushCommand.cpp + +Abstract: + + Implementation of the image push command. + +--*/ + +#include "ImageCommand.h" +#include "CLIExecutionContext.h" +#include "ImageTasks.h" +#include "SessionTasks.h" +#include "Task.h" + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace wsl::windows::wslc { +// Image Push Command +std::vector ImagePushCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::ImageId, true), + Argument::Create(ArgType::Session), + }; +} + +std::wstring ImagePushCommand::ShortDescription() const +{ + return Localization::WSLCCLI_ImagePushDesc(); +} + +std::wstring ImagePushCommand::LongDescription() const +{ + return Localization::WSLCCLI_ImagePushLongDesc(); +} + +void ImagePushCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << CreateSession // + << PushImage; +} +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/RegistryCommand.cpp b/src/windows/wslc/commands/RegistryCommand.cpp new file mode 100644 index 000000000..07ebf9673 --- /dev/null +++ b/src/windows/wslc/commands/RegistryCommand.cpp @@ -0,0 +1,167 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + RegistryCommand.cpp + +Abstract: + + Implementation of the registry command tree (login, logout). + +--*/ + +#include "CLIExecutionContext.h" +#include "RegistryCommand.h" +#include "RegistryTasks.h" +#include "SessionTasks.h" +#include "Task.h" +#include + +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::task; +using namespace wsl::shared; + +namespace { + +std::wstring Prompt(const std::wstring& label, bool maskInput) +{ + // Write without a trailing newline so the cursor stays inline (matching Docker's behavior). + fputws(label.c_str(), stderr); + fflush(stderr); + + std::wstring value; + if (!maskInput) + { + std::getline(std::wcin, value); + return value; + } + + HANDLE input = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + const bool canMask = (input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &mode); + + if (canMask) + { + SetConsoleMode(input, mode & ~ENABLE_ECHO_INPUT); + } + + std::getline(std::wcin, value); + + if (canMask) + { + SetConsoleMode(input, mode); + fputws(L"\n", stderr); + } + + return value; +} + +} // namespace + +namespace wsl::windows::wslc { + +// Registry Root Command +std::vector> RegistryCommand::GetCommands() const +{ + std::vector> commands; + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + return commands; +} + +std::vector RegistryCommand::GetArguments() const +{ + return {}; +} + +std::wstring RegistryCommand::ShortDescription() const +{ + return Localization::WSLCCLI_RegistryCommandDesc(); +} + +std::wstring RegistryCommand::LongDescription() const +{ + return Localization::WSLCCLI_RegistryCommandLongDesc(); +} + +void RegistryCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + OutputHelp(); +} + +// Registry Login Command +std::vector RegistryLoginCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::Password), + Argument::Create(ArgType::PasswordStdin), + Argument::Create(ArgType::Username), + Argument::Create(ArgType::Server), + Argument::Create(ArgType::Session), + }; +} + +std::wstring RegistryLoginCommand::ShortDescription() const +{ + return Localization::WSLCCLI_LoginDesc(); +} + +std::wstring RegistryLoginCommand::LongDescription() const +{ + return Localization::WSLCCLI_LoginLongDesc(); +} + +void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + // Prompt for username if not provided. + if (!context.Args.Contains(ArgType::Username)) + { + context.Args.Add(ArgType::Username, Prompt(L"Username: ", false)); + } + + // Resolve password: --password, --password-stdin, or interactive prompt. + if (!context.Args.Contains(ArgType::Password)) + { + if (context.Args.Contains(ArgType::PasswordStdin)) + { + std::string line; + std::getline(std::cin, line); + context.Args.Add(ArgType::Password, wsl::shared::string::MultiByteToWide(line)); + } + else + { + context.Args.Add(ArgType::Password, Prompt(L"Password: ", true)); + } + } + + context // + << CreateSession << Login; +} + +// Registry Logout Command +std::vector RegistryLogoutCommand::GetArguments() const +{ + return { + Argument::Create(ArgType::Server), + }; +} + +std::wstring RegistryLogoutCommand::ShortDescription() const +{ + return Localization::WSLCCLI_LogoutDesc(); +} + +std::wstring RegistryLogoutCommand::LongDescription() const +{ + return Localization::WSLCCLI_LogoutLongDesc(); +} + +void RegistryLogoutCommand::ExecuteInternal(CLIExecutionContext& context) const +{ + context // + << Logout; +} + +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/RegistryCommand.h b/src/windows/wslc/commands/RegistryCommand.h new file mode 100644 index 000000000..9d6ec68a3 --- /dev/null +++ b/src/windows/wslc/commands/RegistryCommand.h @@ -0,0 +1,70 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + RegistryCommand.h + +Abstract: + + Declaration of the registry command tree (login, logout). + +--*/ +#pragma once +#include "Command.h" + +namespace wsl::windows::wslc { + +// Root registry command: wslc registry [login|logout] +struct RegistryCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"registry"; + RegistryCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + + std::vector> GetCommands() const override; + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Login Command +struct RegistryLoginCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"login"; + + RegistryLoginCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +// Logout Command +struct RegistryLogoutCommand final : public Command +{ + constexpr static std::wstring_view CommandName = L"logout"; + + RegistryLogoutCommand(const std::wstring& parent) : Command(CommandName, parent) + { + } + + std::vector GetArguments() const override; + std::wstring ShortDescription() const override; + std::wstring LongDescription() const override; + +protected: + void ExecuteInternal(CLIExecutionContext& context) const override; +}; + +} // namespace wsl::windows::wslc diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp index 732c19404..ae9af4090 100644 --- a/src/windows/wslc/commands/RootCommand.cpp +++ b/src/windows/wslc/commands/RootCommand.cpp @@ -16,6 +16,7 @@ Module Name: // Include all commands that parent to the root. #include "ContainerCommand.h" #include "ImageCommand.h" +#include "RegistryCommand.h" #include "SessionCommand.h" #include "SettingsCommand.h" #include "VersionCommand.h" @@ -29,6 +30,7 @@ std::vector> RootCommand::GetCommands() const std::vector> commands; commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); @@ -42,6 +44,9 @@ std::vector> RootCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName(), true)); commands.push_back(std::make_unique(FullName())); diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index 4e3c138d9..fd8eacdee 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -12,7 +12,9 @@ Module Name: --*/ #include "ImageService.h" +#include "RegistryService.h" #include "SessionService.h" +#include "UserSettings.h" #include #include @@ -196,7 +198,11 @@ void ImageService::Delete(wsl::windows::wslc::models::Session& session, const st void ImageService::Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback) { - THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), nullptr, callback)); + auto server = GetServerFromImage(image); + auto storedAuth = RegistryService::Get(settings::User().Get(), server); + auto auth = storedAuth.has_value() ? storedAuth->c_str() : nullptr; + + THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), auth, callback)); } InspectImage ImageService::Inspect(wsl::windows::wslc::models::Session& session, const std::string& image) @@ -206,8 +212,33 @@ InspectImage ImageService::Inspect(wsl::windows::wslc::models::Session& session, return wsl::shared::FromJson(inspectData.get()); } -void ImageService::Push() +void ImageService::Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback) +{ + auto server = GetServerFromImage(image); + auto storedAuth = RegistryService::Get(settings::User().Get(), server); + auto auth = storedAuth.value_or(BuildRegistryAuthHeader("", server)); + + THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback)); +} + +std::string ImageService::GetServerFromImage(const std::string& image) { + // Extract the registry domain from an image reference. + // Follows the same logic as Docker's splitDockerDomain in distribution/reference: + // If the part before the first '/' contains a '.' or ':', or is "localhost", + // it's treated as a registry domain. Otherwise it's a Docker Hub repo path. + auto slash = image.find('/'); + if (slash != std::string::npos) + { + auto candidate = image.substr(0, slash); + if (candidate.find('.') != std::string::npos || candidate.find(':') != std::string::npos || candidate == "localhost") + { + return candidate; + } + } + + // TODO: Get default server from the daemon's /info through the wslc session + return std::string(RegistryService::DefaultServer); } void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent) diff --git a/src/windows/wslc/services/ImageService.h b/src/windows/wslc/services/ImageService.h index b6251ca3d..02bfa2d5f 100644 --- a/src/windows/wslc/services/ImageService.h +++ b/src/windows/wslc/services/ImageService.h @@ -36,8 +36,12 @@ class ImageService static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune); static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image); static void Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback); + static void Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback); static void Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent = nullptr); - void Push(); + + // Extracts the registry server address from an image reference. + // Returns the default Docker Hub server if no explicit server is present. + static std::string GetServerFromImage(const std::string& image); void Tag(); void Prune(); }; diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp new file mode 100644 index 000000000..6dae487dd --- /dev/null +++ b/src/windows/wslc/services/RegistryService.cpp @@ -0,0 +1,347 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + RegistryService.cpp + +Abstract: + + This file contains the RegistryService implementation + +--*/ + +#include "RegistryService.h" +#include +#include +#include +#include + +using namespace wsl::shared; +using namespace wsl::windows::common::wslutil; + +namespace { + +wil::unique_hfile OpenJsonFileExclusive(const std::filesystem::path& path) +{ + std::filesystem::create_directories(path.parent_path()); + + wil::unique_hfile handle(CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!handle.is_valid()); + return handle; +} + +wil::unique_hfile OpenJsonFileShared(const std::filesystem::path& path) +{ + wil::unique_hfile handle(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + + if (!handle.is_valid()) + { + THROW_LAST_ERROR_IF(GetLastError() != ERROR_FILE_NOT_FOUND); + } + + return handle; +} + +nlohmann::json ReadJsonFile(const wil::unique_hfile& handle) +{ + LARGE_INTEGER size{}; + THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(handle.get(), &size)); + if (size.QuadPart == 0) + { + return nlohmann::json::object(); + } + + LARGE_INTEGER zero{}; + THROW_IF_WIN32_BOOL_FALSE(SetFilePointerEx(handle.get(), zero, nullptr, FILE_BEGIN)); + + std::string buffer(static_cast(size.QuadPart), '\0'); + DWORD bytesRead = 0; + THROW_IF_WIN32_BOOL_FALSE(ReadFile(handle.get(), buffer.data(), static_cast(buffer.size()), &bytesRead, nullptr)); + buffer.resize(bytesRead); + + try + { + return nlohmann::json::parse(buffer); + } + catch (...) + { + return nlohmann::json::object(); + } +} + +void WriteJsonFile(const wil::unique_hfile& handle, const nlohmann::json& data) +{ + LARGE_INTEGER zero{}; + THROW_IF_WIN32_BOOL_FALSE(SetFilePointerEx(handle.get(), zero, nullptr, FILE_BEGIN)); + THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(handle.get())); + + auto content = data.dump(2); + DWORD written = 0; + THROW_IF_WIN32_BOOL_FALSE(WriteFile(handle.get(), content.data(), static_cast(content.size()), &written, nullptr)); + THROW_IF_WIN32_BOOL_FALSE(FlushFileBuffers(handle.get())); +} + +} // namespace + +namespace wsl::windows::wslc::services { + +void RegistryService::Store(CredentialStoreType backend, const std::string& serverAddress, const std::string& credential) +{ + THROW_HR_IF(E_INVALIDARG, serverAddress.empty()); + THROW_HR_IF(E_INVALIDARG, credential.empty()); + + backend == CredentialStoreType::File ? FileStoreCredential(serverAddress, credential) + : WinCredStoreCredential(serverAddress, credential); +} + +std::optional RegistryService::Get(CredentialStoreType backend, const std::string& serverAddress) +{ + if (serverAddress.empty()) + { + return std::nullopt; + } + + return backend == CredentialStoreType::File ? FileGetCredential(serverAddress) : WinCredGetCredential(serverAddress); +} + +void RegistryService::Erase(CredentialStoreType backend, const std::string& serverAddress) +{ + THROW_HR_IF(E_INVALIDARG, serverAddress.empty()); + + backend == CredentialStoreType::File ? FileEraseCredential(serverAddress) : WinCredEraseCredential(serverAddress); +} + +std::vector RegistryService::List(CredentialStoreType backend) +{ + return backend == CredentialStoreType::File ? FileListCredentials() : WinCredListCredentials(); +} + +// --- WinCred backend --- + +void RegistryService::WinCredStoreCredential(const std::string& serverAddress, const std::string& credential) +{ + auto targetName = wsl::shared::string::MultiByteToWide(serverAddress); + + CREDENTIALW cred{}; + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = targetName.data(); + cred.CredentialBlobSize = static_cast(credential.size()); + cred.CredentialBlob = reinterpret_cast(const_cast(credential.data())); + cred.Persist = CRED_PERSIST_LOCAL_MACHINE; + + THROW_IF_WIN32_BOOL_FALSE(CredWriteW(&cred, 0)); +} + +std::optional RegistryService::WinCredGetCredential(const std::string& serverAddress) +{ + auto targetName = wsl::shared::string::MultiByteToWide(serverAddress); + + PCREDENTIALW cred = nullptr; + if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred)) + { + if (GetLastError() == ERROR_NOT_FOUND) + { + return std::nullopt; + } + + THROW_LAST_ERROR(); + } + + auto cleanup = wil::scope_exit([&]() { CredFree(cred); }); + + if (cred->CredentialBlobSize == 0 || cred->CredentialBlob == nullptr) + { + return std::nullopt; + } + + return std::string(reinterpret_cast(cred->CredentialBlob), cred->CredentialBlobSize); +} + +void RegistryService::WinCredEraseCredential(const std::string& serverAddress) +{ + auto targetName = wsl::shared::string::MultiByteToWide(serverAddress); + + if (!CredDeleteW(targetName.c_str(), CRED_TYPE_GENERIC, 0)) + { + auto error = GetLastError(); + THROW_HR_WITH_USER_ERROR_IF( + E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), error == ERROR_NOT_FOUND); + + THROW_WIN32(error); + } +} + +std::vector RegistryService::WinCredListCredentials() +{ + DWORD count = 0; + PCREDENTIALW* creds = nullptr; + if (!CredEnumerateW(nullptr, 0, &count, &creds)) + { + if (GetLastError() == ERROR_NOT_FOUND) + { + return {}; + } + + THROW_LAST_ERROR(); + } + + auto cleanup = wil::scope_exit([&]() { CredFree(creds); }); + + std::vector result; + result.reserve(count); + for (DWORD i = 0; i < count; ++i) + { + result.push_back(wsl::shared::string::WideToMultiByte(creds[i]->TargetName)); + } + + return result; +} + +// --- File backend --- + +std::filesystem::path RegistryService::GetFilePath() +{ + return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json"; +} + +nlohmann::json RegistryService::ReadFileStore() +{ + auto handle = OpenJsonFileShared(GetFilePath()); + if (!handle.is_valid()) + { + return nlohmann::json::object(); + } + + return ReadJsonFile(handle); +} + +void RegistryService::ModifyFileStore(const std::function& modifier) +{ + auto handle = OpenJsonFileExclusive(GetFilePath()); + auto data = ReadJsonFile(handle); + + if (modifier(data)) + { + WriteJsonFile(handle, data); + } +} + +std::string RegistryService::Protect(const std::string& plaintext) +{ + DATA_BLOB input{}; + input.cbData = static_cast(plaintext.size()); + input.pbData = reinterpret_cast(const_cast(plaintext.data())); + + DATA_BLOB output{}; + THROW_IF_WIN32_BOOL_FALSE(CryptProtectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)); + auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); }); + + return Base64Encode(std::string(reinterpret_cast(output.pbData), output.cbData)); +} + +std::string RegistryService::Unprotect(const std::string& cipherBase64) +{ + auto decoded = Base64Decode(cipherBase64); + + DATA_BLOB input{}; + input.cbData = static_cast(decoded.size()); + input.pbData = reinterpret_cast(decoded.data()); + + DATA_BLOB output{}; + THROW_IF_WIN32_BOOL_FALSE(CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)); + auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); }); + + return std::string(reinterpret_cast(output.pbData), output.cbData); +} + +void RegistryService::FileStoreCredential(const std::string& serverAddress, const std::string& credential) +{ + ModifyFileStore([&](nlohmann::json& data) { + data["registries"][serverAddress] = {{"credential", Protect(credential)}}; + return true; + }); +} + +std::optional RegistryService::FileGetCredential(const std::string& serverAddress) +{ + auto data = ReadFileStore(); + const auto registries = data.find("registries"); + if (registries == data.end() || !registries->is_object()) + { + return std::nullopt; + } + + const auto entry = registries->find(serverAddress); + if (entry == registries->end() || !entry->is_object()) + { + return std::nullopt; + } + + const auto cred = entry->find("credential"); + if (cred == entry->end() || !cred->is_string()) + { + return std::nullopt; + } + + try + { + return Unprotect(cred->get()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION_MSG("Failed to decrypt credential for %hs", serverAddress.c_str()); + return std::nullopt; + } +} + +void RegistryService::FileEraseCredential(const std::string& serverAddress) +{ + bool erased = false; + ModifyFileStore([&](nlohmann::json& data) { + auto registries = data.find("registries"); + if (registries == data.end() || !registries->is_object()) + { + return false; + } + + erased = registries->erase(serverAddress) > 0; + return erased; + }); + + THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased); +} + +std::vector RegistryService::FileListCredentials() +{ + std::vector result; + auto data = ReadFileStore(); + const auto registries = data.find("registries"); + if (registries != data.end() && registries->is_object()) + { + for (const auto& [key, _] : registries->items()) + { + result.push_back(key); + } + } + + return result; +} + +std::string RegistryService::Authenticate( + wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password) +{ + wil::unique_cotaskmem_ansistring identityToken; + THROW_IF_FAILED(session.Get()->Authenticate(serverAddress.c_str(), username.c_str(), password.c_str(), &identityToken)); + + // If the registry returned an identity token, use it. Otherwise fall back to username/password. + if (identityToken && strlen(identityToken.get()) > 0) + { + return BuildRegistryAuthHeader(identityToken.get(), serverAddress); + } + + return BuildRegistryAuthHeader(username, password, serverAddress); +} + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/RegistryService.h b/src/windows/wslc/services/RegistryService.h new file mode 100644 index 000000000..8870e2115 --- /dev/null +++ b/src/windows/wslc/services/RegistryService.h @@ -0,0 +1,62 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + RegistryService.h + +Abstract: + + This file contains the RegistryService definition + +--*/ +#pragma once + +#include "SessionModel.h" +#include "UserSettings.h" + +namespace wsl::windows::wslc::services { + +using wsl::windows::wslc::settings::CredentialStoreType; + +// Credential store that persists opaque credential strings keyed by server address. +// Supports Windows Credential Manager and DPAPI-encrypted JSON file backends. +class RegistryService +{ +public: + static void Store(CredentialStoreType backend, const std::string& serverAddress, const std::string& credential); + static std::optional Get(CredentialStoreType backend, const std::string& serverAddress); + static void Erase(CredentialStoreType backend, const std::string& serverAddress); + static std::vector List(CredentialStoreType backend); + + // Authenticates with a registry via the session's Docker engine. + // Returns a base64-encoded auth header ready to store and pass to push/pull. + static std::string Authenticate( + wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password); + + // Default registry server address used when no explicit server is provided. + static constexpr auto DefaultServer = "https://index.docker.io/v1/"; + +private: + // WinCred helpers + static void WinCredStoreCredential(const std::string& serverAddress, const std::string& credential); + static std::optional WinCredGetCredential(const std::string& serverAddress); + static void WinCredEraseCredential(const std::string& serverAddress); + static std::vector WinCredListCredentials(); + + // File backend helpers + static std::filesystem::path GetFilePath(); + static std::string Protect(const std::string& plaintext); + static std::string Unprotect(const std::string& cipherBase64); + + static void ModifyFileStore(const std::function& modifier); + static nlohmann::json ReadFileStore(); + + static void FileStoreCredential(const std::string& serverAddress, const std::string& credential); + static std::optional FileGetCredential(const std::string& serverAddress); + static void FileEraseCredential(const std::string& serverAddress); + static std::vector FileListCredentials(); +}; + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/settings/UserSettings.cpp b/src/windows/wslc/settings/UserSettings.cpp index 0e4ca688d..11dd4c371 100644 --- a/src/windows/wslc/settings/UserSettings.cpp +++ b/src/windows/wslc/settings/UserSettings.cpp @@ -45,7 +45,10 @@ static constexpr std::string_view s_DefaultSettingsTemplate = " # Default path for session storage. By default, storage is per-session under:\n" " # %LocalAppData%\\wslc\\sessions\\wslc-cli (standard sessions)\n" " # %LocalAppData%\\wslc\\sessions\\wslc-cli-admin (elevated sessions)\n" - " # defaultStoragePath: \"\"\n"; + " # defaultStoragePath: \"\"\n" + "\n" + "# Credential storage backend: \"wincred\" or \"file\" (default: wincred)\n" + "# credentialStore: wincred\n"; // Validate individual setting specializations namespace details { @@ -119,6 +122,20 @@ namespace details { return value; } + WSLC_VALIDATE_SETTING(CredentialStore) + { + if (value == "wincred") + { + return CredentialStoreType::WinCred; + } + if (value == "file") + { + return CredentialStoreType::File; + } + + return std::nullopt; + } + #undef WSLC_VALIDATE_SETTING } // namespace details diff --git a/src/windows/wslc/settings/UserSettings.h b/src/windows/wslc/settings/UserSettings.h index a58b93ab4..c21f5803e 100644 --- a/src/windows/wslc/settings/UserSettings.h +++ b/src/windows/wslc/settings/UserSettings.h @@ -42,6 +42,7 @@ enum class Setting : size_t SessionNetworkingMode, SessionHostFileShareMode, SessionDnsTunneling, + CredentialStore, Max }; @@ -52,6 +53,12 @@ enum class HostFileShareMode VirtioFs }; +enum class CredentialStoreType +{ + WinCred, + File +}; + namespace details { template @@ -83,6 +90,7 @@ namespace details { DEFINE_SETTING_MAPPING(SessionNetworkingMode, std::string, WSLCNetworkingMode, WSLCNetworkingModeVirtioProxy, "session.networkingMode") DEFINE_SETTING_MAPPING(SessionHostFileShareMode, std::string, HostFileShareMode, HostFileShareMode::VirtioFs, "session.hostFileShareMode") DEFINE_SETTING_MAPPING(SessionDnsTunneling, bool, bool, true, "session.dnsTunneling") + DEFINE_SETTING_MAPPING(CredentialStore, std::string, CredentialStoreType, CredentialStoreType::WinCred, "credentialStore") #undef DEFINE_SETTING_MAPPING // clang-format on diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index 9d6a81688..7c8224229 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -138,6 +138,17 @@ void PullImage(CLIExecutionContext& context) services::ImageService::Pull(session, WideToMultiByte(imageId), &callback); } +void PushImage(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::ImageId)); + auto& session = context.Data.Get(); + auto& imageId = context.Args.Get(); + + PullImageCallback callback; + services::ImageService::Push(session, WideToMultiByte(imageId), &callback); +} + void DeleteImage(CLIExecutionContext& context) { WI_ASSERT(context.Data.Contains(Data::Session)); diff --git a/src/windows/wslc/tasks/ImageTasks.h b/src/windows/wslc/tasks/ImageTasks.h index 7b6841ae3..33a52eb65 100644 --- a/src/windows/wslc/tasks/ImageTasks.h +++ b/src/windows/wslc/tasks/ImageTasks.h @@ -22,6 +22,7 @@ void GetImages(CLIExecutionContext& context); void ListImages(CLIExecutionContext& context); void LoadImage(CLIExecutionContext& context); void PullImage(CLIExecutionContext& context); +void PushImage(CLIExecutionContext& context); void DeleteImage(CLIExecutionContext& context); void InspectImages(CLIExecutionContext& context); void SaveImage(CLIExecutionContext& context); diff --git a/src/windows/wslc/tasks/RegistryTasks.cpp b/src/windows/wslc/tasks/RegistryTasks.cpp new file mode 100644 index 000000000..abb71f9f9 --- /dev/null +++ b/src/windows/wslc/tasks/RegistryTasks.cpp @@ -0,0 +1,70 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + RegistryTasks.cpp + +Abstract: + + Implementation of registry command related execution logic. + +--*/ +#include "Argument.h" +#include "CLIExecutionContext.h" +#include "RegistryService.h" +#include "RegistryTasks.h" +#include "Task.h" +#include "UserSettings.h" + +using namespace wsl::shared; +using namespace wsl::windows::common::string; +using namespace wsl::windows::common::wslutil; +using namespace wsl::windows::wslc::execution; +using namespace wsl::windows::wslc::services; +using namespace wsl::windows::wslc::settings; + +namespace wsl::windows::wslc::task { + +void Login(CLIExecutionContext& context) +{ + WI_ASSERT(context.Data.Contains(Data::Session)); + WI_ASSERT(context.Args.Contains(ArgType::Username)); + WI_ASSERT(context.Args.Contains(ArgType::Password)); + + auto& session = context.Data.Get(); + + auto username = WideToMultiByte(context.Args.Get()); + auto password = WideToMultiByte(context.Args.Get()); + + auto serverAddress = std::string(RegistryService::DefaultServer); + + if (context.Args.Contains(ArgType::Server)) + { + serverAddress = WideToMultiByte(context.Args.Get()); + } + + auto credStoreType = User().Get(); + auto auth = RegistryService::Authenticate(session, serverAddress, username, password); + RegistryService::Store(credStoreType, serverAddress, auth); + + PrintMessage(Localization::WSLCCLI_LoginSucceeded()); +} + +void Logout(CLIExecutionContext& context) +{ + auto serverAddress = std::string(RegistryService::DefaultServer); + + if (context.Args.Contains(ArgType::Server)) + { + serverAddress = WideToMultiByte(context.Args.Get()); + } + + auto credStoreType = User().Get(); + RegistryService::Erase(credStoreType, serverAddress); + + PrintMessage(Localization::WSLCCLI_LogoutSucceeded(MultiByteToWide(serverAddress))); +} + +} // namespace wsl::windows::wslc::task diff --git a/src/windows/wslc/tasks/RegistryTasks.h b/src/windows/wslc/tasks/RegistryTasks.h new file mode 100644 index 000000000..59477ce27 --- /dev/null +++ b/src/windows/wslc/tasks/RegistryTasks.h @@ -0,0 +1,22 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + RegistryTasks.h + +Abstract: + + Declaration of registry command execution tasks. + +--*/ +#pragma once +#include "CLIExecutionContext.h" + +using wsl::windows::wslc::execution::CLIExecutionContext; + +namespace wsl::windows::wslc::task { +void Login(CLIExecutionContext& context); +void Logout(CLIExecutionContext& context); +} // namespace wsl::windows::wslc::task diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 4514ce527..da4f31b5a 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -489,8 +489,7 @@ class WSLCTests { std::wstring expectedError = - L"pull access denied for does-not, repository does not exist or may require 'docker login': denied: requested " - L"access to the resource is denied"; + L" repository does not exist or may require 'docker login': denied: requested access to the resource is denied"; VERIFY_ARE_EQUAL(m_defaultSession->PullImage("does-not:exist", nullptr, nullptr), WSLC_E_IMAGE_NOT_FOUND); ValidateCOMErrorMessage(expectedError.c_str()); diff --git a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp index d0fff2ab1..570f08fb3 100644 --- a/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EGlobalTests.cpp @@ -409,6 +409,7 @@ class WSLCE2EGlobalTests std::vector> entries = { {L"container", Localization::WSLCCLI_ContainerCommandDesc()}, {L"image", Localization::WSLCCLI_ImageCommandDesc()}, + {L"registry", Localization::WSLCCLI_RegistryCommandDesc()}, {L"session", Localization::WSLCCLI_SessionCommandDesc()}, {L"settings", Localization::WSLCCLI_SettingsCommandDesc()}, {L"attach", Localization::WSLCCLI_ContainerAttachDesc()}, @@ -420,8 +421,11 @@ class WSLCE2EGlobalTests {L"kill", Localization::WSLCCLI_ContainerKillDesc()}, {L"list", Localization::WSLCCLI_ContainerListDesc()}, {L"load", Localization::WSLCCLI_ImageLoadDesc()}, + {L"login", Localization::WSLCCLI_LoginDesc()}, + {L"logout", Localization::WSLCCLI_LogoutDesc()}, {L"logs", Localization::WSLCCLI_ContainerLogsDesc()}, {L"pull", Localization::WSLCCLI_ImagePullDesc()}, + {L"push", Localization::WSLCCLI_ImagePushDesc()}, {L"remove", Localization::WSLCCLI_ContainerRemoveDesc()}, {L"rmi", Localization::WSLCCLI_ImageRemoveDesc()}, {L"run", Localization::WSLCCLI_ContainerRunDesc()}, diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index cec283e85..d14bddd9a 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -17,6 +17,7 @@ Module Name: #include "WSLCExecutor.h" #include "WSLCE2EHelpers.h" #include +#include extern std::wstring g_testDataPath; @@ -341,4 +342,61 @@ void EnsureSessionIsTerminated(const std::wstring& sessionName) } } } + +wil::com_ptr OpenDefaultElevatedSession() +{ + wil::com_ptr sessionManager; + VERIFY_SUCCEEDED(CoCreateInstance(__uuidof(WSLCSessionManager), nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&sessionManager))); + wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); + + wil::com_ptr session; + VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(L"wslc-cli-admin", &session)); + wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); + + return std::move(session); +} + +std::pair StartLocalRegistry(IWSLCSession& session, const std::string& username, const std::string& password, USHORT port) +{ + EnsureImageIsLoaded({L"wslc-registry", L"latest", GetTestImagePath("wslc-registry:latest")}); + + std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; + + if (!username.empty()) + { + env.push_back(std::format("USERNAME={}", username)); + env.push_back(std::format("PASSWORD={}", password)); + } + + WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env); + launcher.SetEntrypoint({"/entrypoint.sh"}); + launcher.AddPort(port, port, AF_INET); + + auto container = launcher.Launch(session, WSLCContainerStartFlagsNone); + + auto address = std::format("127.0.0.1:{}", port); + auto url = std::format(L"http://{}/v2/", wsl::shared::string::MultiByteToWide(address)); + + int expectedCode = username.empty() ? 200 : 401; + ExpectHttpResponse(url.c_str(), expectedCode, true); + + return {std::move(container), std::move(address)}; +} + +// TODO: Replace with RunWslc("image tag ...") once the 'image tag' CLI command is implemented.s +std::string TagImageForRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress) +{ + auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); + const auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest")); + const auto registryRepo = std::format("{}/{}", registryAddress, repo); + + WSLCTagImageOptions tagOptions{}; + tagOptions.Image = imageName.c_str(); + tagOptions.Repo = registryRepo.c_str(); + tagOptions.Tag = tag.value_or("latest").c_str(); + + VERIFY_SUCCEEDED(session.TagImage(&tagOptions)); + return registryImage; +} + } // namespace WSLCE2ETests \ No newline at end of file diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 9a4ea4a7d..b557faffd 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -18,6 +18,7 @@ Module Name: #include #include #include +#include namespace WSLCE2ETests { @@ -176,4 +177,16 @@ inline void VerifyContainerIsNotListed(const std::wstring& containerNameOrId) { VerifyContainerIsNotListed(containerNameOrId, std::chrono::milliseconds(0), std::chrono::milliseconds(0)); } + +wil::com_ptr OpenDefaultElevatedSession(); + +// Starts a local registry container with host networking using the COM API. +// Returns the running container (holds it alive) and the registry address (e.g. "127.0.0.1:PORT"). +std::pair StartLocalRegistry( + IWSLCSession& session, const std::string& username = "", const std::string& password = "", USHORT port = 5000); + +// TODO: Replace with RunWslc("image tag ...") once the 'image tag' CLI command is implemented. +// Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest"). +std::string TagImageForRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress); + } // namespace WSLCE2ETests \ No newline at end of file diff --git a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp index 84b11484c..441471c7c 100644 --- a/test/windows/wslc/e2e/WSLCE2EImageTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EImageTests.cpp @@ -73,6 +73,7 @@ class WSLCE2EImageTests {L"list", Localization::WSLCCLI_ImageListDesc()}, {L"load", Localization::WSLCCLI_ImageLoadDesc()}, {L"pull", Localization::WSLCCLI_ImagePullDesc()}, + {L"push", Localization::WSLCCLI_ImagePushDesc()}, {L"save", Localization::WSLCCLI_ImageSaveDesc()}, }; diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp new file mode 100644 index 000000000..3db2c56d0 --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -0,0 +1,196 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2EPushPullTests.cpp + +Abstract: + + End-to-end tests for wslc image push and pull against a local registry. + +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" +#include "Argument.h" + +namespace WSLCE2ETests { +using namespace wsl::shared; + +class WSLCE2EPushPullTests +{ + WSLC_TEST_CLASS(WSLCE2EPushPullTests) + + wil::unique_mta_usage_cookie m_mtaCookie; + + TEST_CLASS_SETUP(TestClassSetup) + { + THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Push_HelpCommand) + { + auto result = RunWslc(L"image push --help"); + result.Verify({.Stdout = GetPushHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Push_RootAlias) + { + auto result = RunWslc(L"push --help"); + result.Verify({.Stdout = GetPushRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Pull_HelpCommand) + { + auto result = RunWslc(L"image pull --help"); + result.Verify({.Stdout = GetPullHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Pull_RootAlias) + { + auto result = RunWslc(L"pull --help"); + result.Verify({.Stdout = GetPullRootAliasHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_PushPull) + { + // Ensure the default elevated session exists. + RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0}); + + const auto& debianImage = DebianTestImage(); + EnsureImageIsLoaded(debianImage); + + // Start a local registry without auth. + auto session = OpenDefaultElevatedSession(); + + { + auto [registryContainer, registryAddress] = StartLocalRegistry(*session); + auto registryAddressW = string::MultiByteToWide(registryAddress); + + // Tag the image for the local registry. + auto registryImage = TagImageForRegistry(*session, "debian:latest", registryAddress); + auto registryImageW = string::MultiByteToWide(registryImage); + + auto tagCleanup = wil::scope_exit([&]() { + RunWslc(std::format(L"image delete --force {}", registryImageW)); + }); + + // Push should succeed. + auto result = RunWslc(std::format(L"push {}", registryImageW)); + result.Verify({.ExitCode = 0}); + + // Delete the local copy and pull it back. + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageW), {.ExitCode = 0}); + + result = RunWslc(std::format(L"pull {}", registryImageW)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // Verify the image is now present. + result = RunWslc(L"image list -q"); + result.Verify({.Stderr = L"", .ExitCode = 0}); + VERIFY_IS_TRUE(result.Stdout.has_value()); + VERIFY_IS_TRUE(result.Stdout->find(registryImageW) != std::wstring::npos); + + } + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Push_NonExistentImage) + { + auto result = RunWslc(L"push does-not-exist:latest"); + auto errorMessage = L"An image does not exist locally with the tag: does-not-exist\r\nError code: E_FAIL\r\n"; + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1}); + } + + WSLC_TEST_METHOD(WSLCE2E_Image_Pull_NonExistentImage) + { + auto result = RunWslc(L"pull does-not-exist:latest"); + auto errorMessage = + L"pull access denied for does-not-exist, repository does not exist or may require 'docker login': denied: requested " + L"access to the resource is denied\r\nError code: WSLC_E_IMAGE_NOT_FOUND\r\n"; + result.Verify({.Stdout = L"", .Stderr = errorMessage, .ExitCode = 1}); + } + +private: + std::wstring GetPushHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() << GetPushDescription() << GetPushUsage() << GetAvailableArguments() << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetPushRootAliasHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() << GetPushDescription() << GetPushRootUsage() << GetAvailableArguments() << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetPullHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() << GetPullDescription() << GetPullUsage() << GetAvailableArguments() << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetPullRootAliasHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() << GetPullDescription() << GetPullRootUsage() << GetAvailableArguments() << GetAvailableOptions(); + return output.str(); + } + + std::wstring GetPushDescription() const + { + return Localization::WSLCCLI_ImagePushLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetPullDescription() const + { + return Localization::WSLCCLI_ImagePullLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetPushUsage() const + { + return L"Usage: wslc image push [] \r\n\r\n"; + } + + std::wstring GetPushRootUsage() const + { + return L"Usage: wslc push [] \r\n\r\n"; + } + + std::wstring GetPullUsage() const + { + return L"Usage: wslc image pull [] \r\n\r\n"; + } + + std::wstring GetPullRootUsage() const + { + return L"Usage: wslc pull [] \r\n\r\n"; + } + + std::wstring GetAvailableArguments() const + { + std::wstringstream args; + args << Localization::WSLCCLI_AvailableArguments() << L"\r\n" + << L" image " << Localization::WSLCCLI_ImageIdArgDescription() << L"\r\n" + << L"\r\n"; + return args.str(); + } + + std::wstring GetAvailableOptions() const + { + std::wstringstream options; + options << Localization::WSLCCLI_AvailableOptions() << L"\r\n" + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" + << L" -h,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" + << L"\r\n"; + return options.str(); + } +}; +} // namespace WSLCE2ETests diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp new file mode 100644 index 000000000..7fc9f96c9 --- /dev/null +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -0,0 +1,216 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCE2ERegistryTests.cpp + +Abstract: + + End-to-end tests for wslc registry login/logout auth flows against a local registry. + +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "WSLCExecutor.h" +#include "WSLCE2EHelpers.h" +#include "Argument.h" +#include + +namespace WSLCE2ETests { +using namespace wsl::shared; +using namespace WEX::Logging; + +namespace { + + constexpr auto c_username = "wslctest"; + constexpr auto c_password = "password"; + + void VerifyAuthFailure(const WSLCExecutionResult& result) + { + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"no basic auth credentials") != std::wstring::npos); + } + +} // namespace + +class WSLCE2ERegistryTests +{ + WSLC_TEST_CLASS(WSLCE2ERegistryTests) + + wil::unique_mta_usage_cookie m_mtaCookie; + + TEST_CLASS_SETUP(TestClassSetup) + { + THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); + return true; + } + + WSLC_TEST_METHOD(WSLCE2E_Registry_LoginLogout_PushPull_AuthFlow) + { + const auto& debianImage = DebianTestImage(); + EnsureImageIsLoaded(debianImage); + + // Ensure the default elevated session exists before opening it via COM. + RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0}); + + auto session = OpenDefaultElevatedSession(); + + { + Log::Comment(L"Starting local registry with auth"); + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15001); + auto registryAddressW = string::MultiByteToWide(registryAddress); + Log::Comment(std::format(L"Registry started at {}", registryAddressW).c_str()); + + Log::Comment(L"Tagging image for registry"); + auto registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); + auto registryImageNameW = string::MultiByteToWide(registryImageName); + Log::Comment(std::format(L"Tagged image: {}", registryImageNameW).c_str()); + + auto cleanup = wil::scope_exit([&]() { + RunWslc(std::format(L"image delete --force {}", registryImageNameW)); + RunWslc(std::format(L"logout {}", registryAddressW)); + }); + + // Negative path before login: push and pull should fail. + Log::Comment(L"Testing push without login"); + auto result = RunWslc(std::format(L"push {}", registryImageNameW)); + VerifyAuthFailure(result); + + Log::Comment(L"Deleting tagged image and testing pull without login"); + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); + result = RunWslc(std::format(L"pull {}", registryImageNameW)); + VerifyAuthFailure(result); + + // Login and verify that saved credentials are used for push/pull. + Log::Comment(L"Logging in"); + result = RunWslc(std::format( + L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW)); + result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0}); + + Log::Comment(L"Re-tagging and pushing with auth"); + registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); + result = RunWslc(std::format(L"push {}", registryImageNameW)); + result.Verify({.ExitCode = 0}); + + Log::Comment(L"Deleting and pulling with auth"); + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); + result = RunWslc(std::format(L"pull {}", registryImageNameW)); + result.Verify({.Stderr = L"", .ExitCode = 0}); + + // Logout and verify both pull and push fail again. + Log::Comment(L"Logging out"); + result = RunWslc(std::format(L"logout {}", registryAddressW)); + result.Verify({.Stdout = Localization::WSLCCLI_LogoutSucceeded(registryAddressW) + L"\r\n", .Stderr = L"", .ExitCode = 0}); + + Log::Comment(L"Verifying pull fails after logout"); + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); + result = RunWslc(std::format(L"pull {}", registryImageNameW)); + VerifyAuthFailure(result); + + Log::Comment(L"Verifying push fails after logout"); + registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); + result = RunWslc(std::format(L"push {}", registryImageNameW)); + VerifyAuthFailure(result); + + // Negative path for logout command: second logout should fail. + Log::Comment(L"Verifying second logout fails"); + result = RunWslc(std::format(L"logout {}", registryAddressW)); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"Not logged in to") != std::wstring::npos); + } + } + + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_HelpCommand) + { + auto result = RunWslc(L"registry login --help"); + result.Verify({.Stdout = GetLoginHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + + WSLC_TEST_METHOD(WSLCE2E_Registry_Logout_HelpCommand) + { + auto result = RunWslc(L"registry logout --help"); + result.Verify({.Stdout = GetLogoutHelpMessage(), .Stderr = L"", .ExitCode = 0}); + } + +private: + std::wstring GetLoginHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() << GetLoginDescription() << GetLoginUsage() << GetLoginAvailableArguments() << GetLoginAvailableOptions(); + return output.str(); + } + + std::wstring GetLogoutHelpMessage() const + { + std::wstringstream output; + output << GetWslcHeader() << GetLogoutDescription() << GetLogoutUsage() << GetLogoutAvailableArguments() + << GetLogoutAvailableOptions(); + return output.str(); + } + + std::wstring GetLoginDescription() const + { + return Localization::WSLCCLI_LoginLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetLogoutDescription() const + { + return Localization::WSLCCLI_LogoutLongDesc() + L"\r\n\r\n"; + } + + std::wstring GetLoginUsage() const + { + return L"Usage: wslc registry login [] []\r\n\r\n"; + } + + std::wstring GetLogoutUsage() const + { + return L"Usage: wslc registry logout [] []\r\n\r\n"; + } + + std::wstring GetLoginAvailableArguments() const + { + std::wstringstream args; + args << Localization::WSLCCLI_AvailableArguments() << L"\r\n" + << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n" + << L"\r\n"; + return args.str(); + } + + std::wstring GetLogoutAvailableArguments() const + { + std::wstringstream args; + args << Localization::WSLCCLI_AvailableArguments() << L"\r\n" + << L" server " << Localization::WSLCCLI_LoginServerArgDescription() << L"\r\n" + << L"\r\n"; + return args.str(); + } + + std::wstring GetLoginAvailableOptions() const + { + std::wstringstream options; + options << Localization::WSLCCLI_AvailableOptions() << L"\r\n" + << L" -p,--password " << Localization::WSLCCLI_LoginPasswordArgDescription() << L"\r\n" + << L" --password-stdin " << Localization::WSLCCLI_LoginPasswordStdinArgDescription() << L"\r\n" + << L" -u,--username " << Localization::WSLCCLI_LoginUsernameArgDescription() << L"\r\n" + << L" --session " << Localization::WSLCCLI_SessionIdArgDescription() << L"\r\n" + << L" -h,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" + << L"\r\n"; + return options.str(); + } + + std::wstring GetLogoutAvailableOptions() const + { + std::wstringstream options; + options << Localization::WSLCCLI_AvailableOptions() << L"\r\n" + << L" -h,--help " << Localization::WSLCCLI_HelpArgDescription() << L"\r\n" + << L"\r\n"; + return options.str(); + } +}; +} // namespace WSLCE2ETests From 1f07291c828f85556b57d09ae8884c20a3dfdde5 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 13 Apr 2026 16:18:14 -0700 Subject: [PATCH 35/76] Fix bad merge --- src/windows/WslcSDK/wslcsdk.h | 54 +---------------------------------- test/windows/WSLCTests.cpp | 22 ++++++++++---- 2 files changed, 18 insertions(+), 58 deletions(-) diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index 3f42227d3..3a9dfccc3 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -471,58 +471,6 @@ typedef struct WslcPushImageOptions STDAPI WslcPushSessionImage(_In_ WslcSession session, _In_ const WslcPushImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage); -// Authenticates with a container registry and returns an identity token. -// -// Parameters: -// session -// A valid WslcSession handle. -// -// serverAddress -// The registry server address (e.g. "127.0.0.1:5000"). -// -// username -// The username for authentication. -// -// password -// The password for authentication. -// -// identityToken -// On success, receives a pointer to a null-terminated ANSI string -// containing the identity token. -// -// The string is allocated using CoTaskMemAlloc. The caller takes -// ownership of the returned memory and must free it by calling -// CoTaskMemFree when it is no longer needed. -// -// Return Value: -// S_OK on success. Otherwise, an HRESULT error code indicating the failure. -STDAPI WslcSessionAuthenticate( - _In_ WslcSession session, - _In_z_ PCSTR serverAddress, - _In_z_ PCSTR username, - _In_z_ PCSTR password, - _Outptr_result_z_ PSTR* identityToken, - _Outptr_opt_result_z_ PWSTR* errorMessage); - -typedef struct WslcTagImageOptions -{ - _In_z_ PCSTR image; // Source image name or ID. - _In_z_ PCSTR repo; // Target repository name. - _In_z_ PCSTR tag; // Target tag name. -} WslcTagImageOptions; - -STDAPI WslcTagSessionImage(_In_ WslcSession session, _In_ const WslcTagImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage); - -typedef struct WslcPushImageOptions -{ - _In_z_ PCSTR image; - _In_z_ PCSTR registryAuth; // Base64-encoded X-Registry-Auth header value. - _In_opt_ WslcContainerImageProgressCallback progressCallback; - _In_opt_ PVOID progressCallbackContext; -} WslcPushImageOptions; - -STDAPI WslcPushSessionImage(_In_ WslcSession session, _In_ const WslcPushImageOptions* options, _Outptr_opt_result_z_ PWSTR* errorMessage); - // Authenticates with a container registry and returns an identity token. // // Parameters: @@ -616,4 +564,4 @@ typedef __callback void(CALLBACK* WslcInstallCallback)(_In_ WslcComponentFlags c STDAPI WslcInstallWithDependencies(_In_opt_ WslcInstallCallback progressCallback, _In_opt_ PVOID context); -EXTERN_C_END +EXTERN_C_END \ No newline at end of file diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index a1f7b855f..dba64213c 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -402,7 +402,7 @@ class WSLCTests { auto settings = GetDefaultSessionSettings(nullptr); wil::com_ptr session; - VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG); + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME); } // Reject DisplayName at exact boundary (no room for null terminator). @@ -410,7 +410,7 @@ class WSLCTests std::wstring boundaryName(std::size(WSLCSessionInformation{}.DisplayName), L'x'); auto settings = GetDefaultSessionSettings(boundaryName.c_str()); wil::com_ptr session; - VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG); + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME); } // Reject too long DisplayName. @@ -418,7 +418,7 @@ class WSLCTests std::wstring longName(std::size(WSLCSessionInformation{}.DisplayName) + 1, L'x'); auto settings = GetDefaultSessionSettings(longName.c_str()); wil::com_ptr session; - VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), E_INVALIDARG); + VERIFY_ARE_EQUAL(sessionManager->CreateSession(&settings, WSLCSessionFlagsNone, &session), WSLC_E_INVALID_SESSION_NAME); } // Validate that creating a session on a non-existing storage fails if WSLCSessionStorageFlagsNoCreate is set. @@ -501,7 +501,8 @@ class WSLCTests { std::wstring expectedError = - L" repository does not exist or may require 'docker login': denied: requested access to the resource is denied"; + L"pull access denied for does-not, repository does not exist or may require 'docker login': denied: requested " + L"access to the resource is denied"; VERIFY_ARE_EQUAL(m_defaultSession->PullImage("does-not:exist", nullptr, nullptr), WSLC_E_IMAGE_NOT_FOUND); ValidateCOMErrorMessage(expectedError.c_str()); @@ -2324,7 +2325,18 @@ class WSLCTests std::thread thread(readDmesg); // Needs to be created before the VM starts, to avoid a pipe deadlock. + // Ensure the thread is joined even if CreateSession throws, to avoid std::terminate. + auto threadGuard = wil::scope_exit([&]() { + write.reset(); + if (thread.joinable()) + { + thread.join(); + } + }); + auto session = CreateSession(settings); + threadGuard.release(); // CreateSession succeeded, detach scope_exit below takes over. + auto detach = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { session.reset(); if (thread.joinable()) @@ -7036,4 +7048,4 @@ class WSLCTests ValidateProcessOutput(initProcess, {{1, "OK\n"}}); } -}; +}; \ No newline at end of file From 2a480dd34fbd59ecfc8f14047ab7f192b7c03a4c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 13 Apr 2026 16:27:28 -0700 Subject: [PATCH 36/76] Fix another bad merge issues --- localization/strings/en-US/Resources.resw | 1 + src/windows/common/WSLCContainerLauncher.cpp | 1 - src/windows/common/WSLCContainerLauncher.h | 1 - src/windows/common/wslutil.h | 5 --- src/windows/wslc/services/ImageService.cpp | 42 ++++++++++---------- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 3166be101..d143b29f9 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2325,6 +2325,7 @@ For privacy information about this product please visit https://aka.ms/privacy.< Manage registry credentials, including logging in and out of container registries. + Tag an image. diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index 78eb56403..7b31353e9 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -12,7 +12,6 @@ Module Name: --*/ -#include "precomp.h" #include "WSLCContainerLauncher.h" using wsl::windows::common::ClientRunningWSLCProcess; diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index 4643bc875..900a24c06 100644 --- a/src/windows/common/WSLCContainerLauncher.h +++ b/src/windows/common/WSLCContainerLauncher.h @@ -79,7 +79,6 @@ class WSLCContainerLauncher : private WSLCProcessLauncher void SetDnsSearchDomains(std::vector&& DnsSearchDomains); void SetDnsOptions(std::vector&& DnsOptions); - using WSLCProcessLauncher::FormatResult; using WSLCProcessLauncher::SetUser; using WSLCProcessLauncher::SetWorkingDirectory; diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index 5ae95f0c9..a9ddcb9a2 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -333,12 +333,7 @@ winrt::Windows::Management::Deployment::PackageVolume GetSystemVolume(); std::string Base64Encode(const std::string& input); std::string Base64Decode(const std::string& encoded); -// Builds the base64-encoded X-Registry-Auth header value used by Docker APIs -// (PullImage, PushImage, etc.) from the given credentials. std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress); - -// Builds the base64-encoded X-Registry-Auth header value from an identity token -// returned by Authenticate(). std::string BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress); } // namespace wsl::windows::common::wslutil diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index d7269fc2c..05a535b6f 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -65,6 +65,26 @@ wil::unique_hfile ResolveBuildFile(const std::filesystem::path& contextPath) THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcBuildFileNotFound(contextPath)); } +std::string GetServerFromImage(const std::string& image) +{ + // Extract the registry domain from an image reference. + // Follows the same logic as Docker's splitDockerDomain in distribution/reference: + // If the part before the first '/' contains a '.' or ':', or is "localhost", + // it's treated as a registry domain. Otherwise it's a Docker Hub repo path. + auto slash = image.find('/'); + if (slash != std::string::npos) + { + auto candidate = image.substr(0, slash); + if (candidate.find('.') != std::string::npos || candidate.find(':') != std::string::npos || candidate == "localhost") + { + return candidate; + } + } + + // TODO: Get default server from the daemon's /info through the wslc session + return std::string(wsl::windows::wslc::services::RegistryService::DefaultServer); +} + } // namespace namespace wsl::windows::wslc::services { @@ -233,31 +253,11 @@ void ImageService::Push(wsl::windows::wslc::models::Session& session, const std: { auto server = GetServerFromImage(image); auto storedAuth = RegistryService::Get(settings::User().Get(), server); - auto auth = storedAuth.value_or(BuildRegistryAuthHeader("", server)); + auto auth = storedAuth.value_or(BuildRegistryAuthHeader("","", server)); THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback)); } -std::string ImageService::GetServerFromImage(const std::string& image) -{ - // Extract the registry domain from an image reference. - // Follows the same logic as Docker's splitDockerDomain in distribution/reference: - // If the part before the first '/' contains a '.' or ':', or is "localhost", - // it's treated as a registry domain. Otherwise it's a Docker Hub repo path. - auto slash = image.find('/'); - if (slash != std::string::npos) - { - auto candidate = image.substr(0, slash); - if (candidate.find('.') != std::string::npos || candidate.find(':') != std::string::npos || candidate == "localhost") - { - return candidate; - } - } - - // TODO: Get default server from the daemon's /info through the wslc session - return std::string(RegistryService::DefaultServer); -} - void ImageService::Save(wsl::windows::wslc::models::Session& session, const std::string& image, const std::wstring& output, HANDLE cancelEvent) { wil::unique_hfile outputFile{ From 5b8c26f351a4ab6130349e5d3ab31885316e8ef1 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 13 Apr 2026 16:42:16 -0700 Subject: [PATCH 37/76] Fix session name not found --- test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index efb87b577..bcdaf7cb4 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -375,7 +375,7 @@ wil::com_ptr OpenDefaultElevatedSession() wsl::windows::common::security::ConfigureForCOMImpersonation(sessionManager.get()); wil::com_ptr session; - VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(L"wslc-cli-admin", &session)); + VERIFY_SUCCEEDED(sessionManager->OpenSessionByName(nullptr, &session)); wsl::windows::common::security::ConfigureForCOMImpersonation(session.get()); return std::move(session); From f9219140ab0115371c3092f4b0d6bb68571a1a7f Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 13 Apr 2026 16:47:56 -0700 Subject: [PATCH 38/76] add wslc cred prefix --- src/windows/wslc/services/RegistryService.cpp | 23 +++++++++++++++---- src/windows/wslc/services/RegistryService.h | 2 ++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index 33e2e0dbe..b4874018a 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -21,6 +21,8 @@ Module Name: using namespace wsl::shared; using namespace wsl::windows::common::wslutil; +static constexpr auto WinCredPrefix = L"wslc-credential/"; + namespace { wil::unique_hfile OpenJsonFileExclusive(const std::filesystem::path& path) @@ -124,9 +126,14 @@ std::vector RegistryService::List() // --- WinCred backend --- +std::wstring RegistryService::WinCredTargetName(const std::string& serverAddress) +{ + return std::wstring(WinCredPrefix) + wsl::shared::string::MultiByteToWide(serverAddress); +} + void RegistryService::WinCredStoreCredential(const std::string& serverAddress, const std::string& credential) { - auto targetName = wsl::shared::string::MultiByteToWide(serverAddress); + auto targetName = WinCredTargetName(serverAddress); CREDENTIALW cred{}; cred.Type = CRED_TYPE_GENERIC; @@ -140,7 +147,7 @@ void RegistryService::WinCredStoreCredential(const std::string& serverAddress, c std::optional RegistryService::WinCredGetCredential(const std::string& serverAddress) { - auto targetName = wsl::shared::string::MultiByteToWide(serverAddress); + auto targetName = WinCredTargetName(serverAddress); PCREDENTIALW cred = nullptr; if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred)) @@ -165,7 +172,7 @@ std::optional RegistryService::WinCredGetCredential(const std::stri void RegistryService::WinCredEraseCredential(const std::string& serverAddress) { - auto targetName = wsl::shared::string::MultiByteToWide(serverAddress); + auto targetName = WinCredTargetName(serverAddress); if (!CredDeleteW(targetName.c_str(), CRED_TYPE_GENERIC, 0)) { @@ -179,9 +186,12 @@ void RegistryService::WinCredEraseCredential(const std::string& serverAddress) std::vector RegistryService::WinCredListCredentials() { + auto prefix = std::wstring(WinCredPrefix); + auto filter = prefix + L"*"; + DWORD count = 0; PCREDENTIALW* creds = nullptr; - if (!CredEnumerateW(nullptr, 0, &count, &creds)) + if (!CredEnumerateW(filter.c_str(), 0, &count, &creds)) { if (GetLastError() == ERROR_NOT_FOUND) { @@ -195,9 +205,12 @@ std::vector RegistryService::WinCredListCredentials() std::vector result; result.reserve(count); + for (DWORD i = 0; i < count; ++i) { - result.push_back(wsl::shared::string::WideToMultiByte(creds[i]->TargetName)); + // Strip the prefix to return the bare server address. + std::wstring_view name(creds[i]->TargetName); + result.push_back(wsl::shared::string::WideToMultiByte(std::wstring(name.substr(prefix.size())))); } return result; diff --git a/src/windows/wslc/services/RegistryService.h b/src/windows/wslc/services/RegistryService.h index 8b0386029..2e5b694b0 100644 --- a/src/windows/wslc/services/RegistryService.h +++ b/src/windows/wslc/services/RegistryService.h @@ -39,6 +39,8 @@ class RegistryService static constexpr auto DefaultServer = "https://index.docker.io/v1/"; private: + static std::wstring WinCredTargetName(const std::string& serverAddress); + // WinCred helpers static void WinCredStoreCredential(const std::string& serverAddress, const std::string& credential); static std::optional WinCredGetCredential(const std::string& serverAddress); From 04238d5464cfbcb3b8c7a2709df391f79361d57d Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 13 Apr 2026 16:48:36 -0700 Subject: [PATCH 39/76] add wslc cred prefix --- src/windows/wslc/services/ImageService.cpp | 2 +- src/windows/wslc/services/RegistryService.cpp | 3 +-- test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 5 +---- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index 3d9dafab2..591431f35 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -252,7 +252,7 @@ void ImageService::Push(wsl::windows::wslc::models::Session& session, const std: { auto server = GetServerFromImage(image); auto storedAuth = RegistryService::Get(server); - auto auth = storedAuth.value_or(BuildRegistryAuthHeader("","", server)); + auto auth = storedAuth.value_or(BuildRegistryAuthHeader("", "", server)); THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback)); } diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index b4874018a..05fb421d8 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -95,8 +95,7 @@ void RegistryService::Store(const std::string& serverAddress, const std::string& THROW_HR_IF(E_INVALIDARG, credential.empty()); auto backend = settings::User().Get(); - backend == CredentialStoreType::File ? FileStoreCredential(serverAddress, credential) - : WinCredStoreCredential(serverAddress, credential); + backend == CredentialStoreType::File ? FileStoreCredential(serverAddress, credential) : WinCredStoreCredential(serverAddress, credential); } std::optional RegistryService::Get(const std::string& serverAddress) diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index bcdaf7cb4..797b31f5e 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -386,7 +386,7 @@ std::pair StartLocalRegistry(IWSLCSession& se EnsureImageIsLoaded({L"wslc-registry", L"latest", GetTestImagePath("wslc-registry:latest")}); std::vector env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)}; - + if (!username.empty()) { env.push_back(std::format("USERNAME={}", username)); diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index 3db2c56d0..bf7732fed 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -76,9 +76,7 @@ class WSLCE2EPushPullTests auto registryImage = TagImageForRegistry(*session, "debian:latest", registryAddress); auto registryImageW = string::MultiByteToWide(registryImage); - auto tagCleanup = wil::scope_exit([&]() { - RunWslc(std::format(L"image delete --force {}", registryImageW)); - }); + auto tagCleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImageW)); }); // Push should succeed. auto result = RunWslc(std::format(L"push {}", registryImageW)); @@ -95,7 +93,6 @@ class WSLCE2EPushPullTests result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.Stdout.has_value()); VERIFY_IS_TRUE(result.Stdout->find(registryImageW) != std::wstring::npos); - } } From 2c8c1614956da95a7cbdd0b9c3f5a8729dd066c2 Mon Sep 17 00:00:00 2001 From: Kevin Vega <40717198+kvega005@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:51:55 -0700 Subject: [PATCH 40/76] Update test/windows/wslc/e2e/WSLCE2EHelpers.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index 797b31f5e..8ad3d879e 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -408,7 +408,7 @@ std::pair StartLocalRegistry(IWSLCSession& se return {std::move(container), std::move(address)}; } -// TODO: Replace with RunWslc("image tag ...") once the 'image tag' CLI command is implemented.s +// TODO: Replace with RunWslc("image tag ...") once the 'image tag' CLI command is implemented. std::string TagImageForRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress) { auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); From c254dad5b4d3ecaf2abfd6afcdbe14afce8ea96b Mon Sep 17 00:00:00 2001 From: kvega005 Date: Mon, 13 Apr 2026 19:33:17 -0700 Subject: [PATCH 41/76] Address copilot comments --- localization/strings/en-US/Resources.resw | 8 ++ src/windows/wslc/commands/RegistryCommand.cpp | 56 +++++++------ src/windows/wslc/commands/RegistryCommand.h | 1 + .../windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 80 +++++++++++++++---- 4 files changed, 108 insertions(+), 37 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index b0d53c7a2..db37b688e 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2333,6 +2333,14 @@ For privacy information about this product please visit https://aka.ms/privacy.< Take the Password or Personal Access Token (PAT) from stdin {Locked="PAT"}{Locked="stdin"}Technical terms should not be translated + + --password and --password-stdin are mutually exclusive + {Locked="--password"}{Locked="--password-stdin"}CLI flag names should not be translated + + + Must provide --username with --password-stdin + {Locked="--username"}{Locked="--password-stdin"}CLI flag names should not be translated + Manage registry credentials. diff --git a/src/windows/wslc/commands/RegistryCommand.cpp b/src/windows/wslc/commands/RegistryCommand.cpp index 07ebf9673..57e939431 100644 --- a/src/windows/wslc/commands/RegistryCommand.cpp +++ b/src/windows/wslc/commands/RegistryCommand.cpp @@ -25,35 +25,32 @@ using namespace wsl::shared; namespace { -std::wstring Prompt(const std::wstring& label, bool maskInput) +auto MaskInput() { - // Write without a trailing newline so the cursor stays inline (matching Docker's behavior). - fputws(label.c_str(), stderr); - fflush(stderr); - - std::wstring value; - if (!maskInput) - { - std::getline(std::wcin, value); - return value; - } - HANDLE input = GetStdHandle(STD_INPUT_HANDLE); DWORD mode = 0; - const bool canMask = (input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &mode); - if (canMask) + if ((input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &mode)) { SetConsoleMode(input, mode & ~ENABLE_ECHO_INPUT); + return wil::scope_exit(std::function([input, mode] { + SetConsoleMode(input, mode); + std::wcerr << L'\n'; + })); } - std::getline(std::wcin, value); + return wil::scope_exit(std::function([] {})); +} - if (canMask) - { - SetConsoleMode(input, mode); - fputws(L"\n", stderr); - } +std::wstring Prompt(const std::wstring& label, bool maskInput) +{ + // Write without a trailing newline so the cursor stays inline (matching Docker's behavior). + std::wcerr << label; + + auto restoreConsole = maskInput ? MaskInput() : wil::scope_exit(std::function([] {})); + + std::wstring value; + std::getline(std::wcin, value); return value; } @@ -113,6 +110,19 @@ std::wstring RegistryLoginCommand::LongDescription() const return Localization::WSLCCLI_LoginLongDesc(); } +void RegistryLoginCommand::ValidateArgumentsInternal(const ArgMap& execArgs) const +{ + if (execArgs.Contains(ArgType::Password) && execArgs.Contains(ArgType::PasswordStdin)) + { + throw CommandException(Localization::WSLCCLI_LoginPasswordAndStdinMutuallyExclusive()); + } + + if (execArgs.Contains(ArgType::PasswordStdin) && !execArgs.Contains(ArgType::Username)) + { + throw CommandException(Localization::WSLCCLI_LoginPasswordStdinRequiresUsername()); + } +} + void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const { // Prompt for username if not provided. @@ -126,9 +136,9 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const { if (context.Args.Contains(ArgType::PasswordStdin)) { - std::string line; - std::getline(std::cin, line); - context.Args.Add(ArgType::Password, wsl::shared::string::MultiByteToWide(line)); + std::wstring line; + std::getline(std::wcin, line); + context.Args.Add(ArgType::Password, std::move(line)); } else { diff --git a/src/windows/wslc/commands/RegistryCommand.h b/src/windows/wslc/commands/RegistryCommand.h index 9d6ec68a3..ed8d04e3e 100644 --- a/src/windows/wslc/commands/RegistryCommand.h +++ b/src/windows/wslc/commands/RegistryCommand.h @@ -47,6 +47,7 @@ struct RegistryLoginCommand final : public Command std::wstring LongDescription() const override; protected: + void ValidateArgumentsInternal(const ArgMap& execArgs) const override; void ExecuteInternal(CLIExecutionContext& context) const override; }; diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index 7fc9f96c9..572890d68 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -35,6 +35,11 @@ namespace { VERIFY_IS_TRUE(result.Stderr->find(L"no basic auth credentials") != std::wstring::npos); } + void VerifyLogoutSucceeds(const std::wstring& registryAddress) + { + auto result = RunWslc(std::format(L"logout {}", registryAddress)); + result.Verify({.Stdout = Localization::WSLCCLI_LogoutSucceeded(registryAddress) + L"\r\n", .Stderr = L"", .ExitCode = 0}); + } } // namespace class WSLCE2ERegistryTests @@ -60,15 +65,11 @@ class WSLCE2ERegistryTests auto session = OpenDefaultElevatedSession(); { - Log::Comment(L"Starting local registry with auth"); auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15001); auto registryAddressW = string::MultiByteToWide(registryAddress); - Log::Comment(std::format(L"Registry started at {}", registryAddressW).c_str()); - Log::Comment(L"Tagging image for registry"); auto registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); auto registryImageNameW = string::MultiByteToWide(registryImageName); - Log::Comment(std::format(L"Tagged image: {}", registryImageNameW).c_str()); auto cleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImageNameW)); @@ -76,7 +77,6 @@ class WSLCE2ERegistryTests }); // Negative path before login: push and pull should fail. - Log::Comment(L"Testing push without login"); auto result = RunWslc(std::format(L"push {}", registryImageNameW)); VerifyAuthFailure(result); @@ -86,38 +86,30 @@ class WSLCE2ERegistryTests VerifyAuthFailure(result); // Login and verify that saved credentials are used for push/pull. - Log::Comment(L"Logging in"); result = RunWslc(std::format( L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW)); result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0}); - Log::Comment(L"Re-tagging and pushing with auth"); registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); result = RunWslc(std::format(L"push {}", registryImageNameW)); result.Verify({.ExitCode = 0}); - Log::Comment(L"Deleting and pulling with auth"); RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); result = RunWslc(std::format(L"pull {}", registryImageNameW)); result.Verify({.Stderr = L"", .ExitCode = 0}); // Logout and verify both pull and push fail again. - Log::Comment(L"Logging out"); - result = RunWslc(std::format(L"logout {}", registryAddressW)); - result.Verify({.Stdout = Localization::WSLCCLI_LogoutSucceeded(registryAddressW) + L"\r\n", .Stderr = L"", .ExitCode = 0}); + VerifyLogoutSucceeds(registryAddressW); - Log::Comment(L"Verifying pull fails after logout"); RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); result = RunWslc(std::format(L"pull {}", registryImageNameW)); VerifyAuthFailure(result); - Log::Comment(L"Verifying push fails after logout"); registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); result = RunWslc(std::format(L"push {}", registryImageNameW)); VerifyAuthFailure(result); // Negative path for logout command: second logout should fail. - Log::Comment(L"Verifying second logout fails"); result = RunWslc(std::format(L"logout {}", registryAddressW)); VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); VERIFY_IS_TRUE(result.Stderr.has_value()); @@ -137,6 +129,66 @@ class WSLCE2ERegistryTests result.Verify({.Stdout = GetLogoutHelpMessage(), .Stderr = L"", .ExitCode = 0}); } + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordAndStdinMutuallyExclusive) + { + auto result = RunWslc(L"login -u testuser -p testpass --password-stdin localhost:15099"); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"--password and --password-stdin are mutually exclusive") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_PasswordStdinRequiresUsername) + { + auto result = RunWslc(L"login --password-stdin localhost:15099"); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"Must provide --username with --password-stdin") != std::wstring::npos); + } + + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_CredentialInputMethods) + { + auto session = OpenDefaultElevatedSession(); + + { + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15002); + auto registryAddressW = string::MultiByteToWide(registryAddress); + auto usernameW = string::MultiByteToWide(c_username); + auto passwordW = string::MultiByteToWide(c_password); + + // Login with -u and -p flags. + { + auto result = RunWslc(std::format(L"login -u {} -p {} {}", usernameW, passwordW, registryAddressW)); + result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0}); + + VerifyLogoutSucceeds(registryAddressW); + } + + // Login with -u and --password-stdin. + { + auto interactive = RunWslcInteractive(std::format(L"login -u {} --password-stdin {}", usernameW, registryAddressW)); + interactive.WriteLine(c_password); + interactive.CloseStdin(); + auto exitCode = interactive.Wait(); + VERIFY_ARE_EQUAL(0, exitCode, L"Login with --password-stdin should succeed"); + + VerifyLogoutSucceeds(registryAddressW); + } + + // Login with interactive prompts (no flags). + { + auto interactive = RunWslcInteractive(std::format(L"login {}", registryAddressW)); + interactive.ExpectStderr("Username: "); + interactive.WriteLine(c_username); + interactive.ExpectStderr("Password: "); + interactive.WriteLine(c_password); + auto exitCode = interactive.Wait(); + VERIFY_ARE_EQUAL(0, exitCode, L"Interactive login should succeed"); + + VerifyLogoutSucceeds(registryAddressW); + } + } + } + private: std::wstring GetLoginHelpMessage() const { From 6428b91888cde17de6bbe68a526336150600398d Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 08:32:24 -0700 Subject: [PATCH 42/76] Address feedback and cleanup --- src/windows/wslc/services/RegistryService.cpp | 138 ++++++++---------- src/windows/wslc/services/RegistryService.h | 7 +- test/windows/wslc/e2e/WSLCE2EHelpers.cpp | 15 +- test/windows/wslc/e2e/WSLCE2EHelpers.h | 3 +- .../windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 13 +- .../windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 29 ++-- 6 files changed, 87 insertions(+), 118 deletions(-) diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index 05fb421d8..7efc91877 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -15,6 +15,7 @@ Module Name: #include "RegistryService.h" #include #include +#include #include #include @@ -23,25 +24,37 @@ using namespace wsl::windows::common::wslutil; static constexpr auto WinCredPrefix = L"wslc-credential/"; +using unique_credential = wil::unique_any; +using unique_credential_array = wil::unique_any; + namespace { -wil::unique_hfile OpenJsonFileExclusive(const std::filesystem::path& path) +std::filesystem::path GetFilePath() { - std::filesystem::create_directories(path.parent_path()); + return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json"; +} + +wil::unique_hfile RetryOpenFileOnSharingViolation(const std::function& openFunc) +{ + return wsl::shared::retry::RetryWithTimeout(openFunc, std::chrono::milliseconds(100), std::chrono::seconds(1), []() { + return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); + }); +} - wil::unique_hfile handle(CreateFileW(path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); +wil::unique_hfile OpenFileExclusive() +{ + wil::unique_hfile handle( + CreateFileW(GetFilePath().c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); THROW_LAST_ERROR_IF(!handle.is_valid()); + return handle; } -wil::unique_hfile OpenJsonFileShared(const std::filesystem::path& path) +wil::unique_hfile OpenFileShared() { - wil::unique_hfile handle(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); - - if (!handle.is_valid()) - { - THROW_LAST_ERROR_IF(GetLastError() != ERROR_FILE_NOT_FOUND); - } + wil::unique_hfile handle(CreateFileW( + GetFilePath().c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!handle.is_valid() && GetLastError() != ERROR_FILE_NOT_FOUND); return handle; } @@ -117,7 +130,7 @@ void RegistryService::Erase(const std::string& serverAddress) backend == CredentialStoreType::File ? FileEraseCredential(serverAddress) : WinCredEraseCredential(serverAddress); } -std::vector RegistryService::List() +std::vector RegistryService::List() { auto backend = settings::User().Get(); return backend == CredentialStoreType::File ? FileListCredentials() : WinCredListCredentials(); @@ -148,25 +161,19 @@ std::optional RegistryService::WinCredGetCredential(const std::stri { auto targetName = WinCredTargetName(serverAddress); - PCREDENTIALW cred = nullptr; + unique_credential cred; if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred)) { - if (GetLastError() == ERROR_NOT_FOUND) - { - return std::nullopt; - } - - THROW_LAST_ERROR(); + THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); + return std::nullopt; } - auto cleanup = wil::scope_exit([&]() { CredFree(cred); }); - - if (cred->CredentialBlobSize == 0 || cred->CredentialBlob == nullptr) + if (cred.get()->CredentialBlobSize == 0 || cred.get()->CredentialBlob == nullptr) { return std::nullopt; } - return std::string(reinterpret_cast(cred->CredentialBlob), cred->CredentialBlobSize); + return std::string(reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize); } void RegistryService::WinCredEraseCredential(const std::string& serverAddress) @@ -183,33 +190,27 @@ void RegistryService::WinCredEraseCredential(const std::string& serverAddress) } } -std::vector RegistryService::WinCredListCredentials() +std::vector RegistryService::WinCredListCredentials() { auto prefix = std::wstring(WinCredPrefix); auto filter = prefix + L"*"; DWORD count = 0; - PCREDENTIALW* creds = nullptr; + unique_credential_array creds; if (!CredEnumerateW(filter.c_str(), 0, &count, &creds)) { - if (GetLastError() == ERROR_NOT_FOUND) - { - return {}; - } - - THROW_LAST_ERROR(); + THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); + return {}; } - auto cleanup = wil::scope_exit([&]() { CredFree(creds); }); - - std::vector result; + std::vector result; result.reserve(count); for (DWORD i = 0; i < count; ++i) { // Strip the prefix to return the bare server address. - std::wstring_view name(creds[i]->TargetName); - result.push_back(wsl::shared::string::WideToMultiByte(std::wstring(name.substr(prefix.size())))); + std::wstring_view name(creds.get()[i]->TargetName); + result.emplace_back(name.substr(prefix.size())); } return result; @@ -217,14 +218,9 @@ std::vector RegistryService::WinCredListCredentials() // --- File backend --- -std::filesystem::path RegistryService::GetFilePath() -{ - return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json"; -} - nlohmann::json RegistryService::ReadFileStore() { - auto handle = OpenJsonFileShared(GetFilePath()); + auto handle = RetryOpenFileOnSharingViolation(OpenFileShared); if (!handle.is_valid()) { return nlohmann::json::object(); @@ -235,7 +231,9 @@ nlohmann::json RegistryService::ReadFileStore() void RegistryService::ModifyFileStore(const std::function& modifier) { - auto handle = OpenJsonFileExclusive(GetFilePath()); + auto handle = RetryOpenFileOnSharingViolation(OpenFileExclusive); + WI_VERIFY(handle.is_valid()); + auto data = ReadJsonFile(handle); if (modifier(data)) @@ -274,8 +272,18 @@ std::string RegistryService::Unprotect(const std::string& cipherBase64) void RegistryService::FileStoreCredential(const std::string& serverAddress, const std::string& credential) { + auto filePath = GetFilePath(); + + if (!std::filesystem::exists(filePath)) + { + std::filesystem::create_directories(filePath.parent_path()); + + wil::unique_hfile credFile(CreateFileW(filePath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!credFile.is_valid()); + } + ModifyFileStore([&](nlohmann::json& data) { - data["registries"][serverAddress] = {{"credential", Protect(credential)}}; + data[serverAddress] = Protect(credential); return true; }); } @@ -283,62 +291,36 @@ void RegistryService::FileStoreCredential(const std::string& serverAddress, cons std::optional RegistryService::FileGetCredential(const std::string& serverAddress) { auto data = ReadFileStore(); - const auto registries = data.find("registries"); - if (registries == data.end() || !registries->is_object()) - { - return std::nullopt; - } - - const auto entry = registries->find(serverAddress); - if (entry == registries->end() || !entry->is_object()) - { - return std::nullopt; - } - const auto cred = entry->find("credential"); - if (cred == entry->end() || !cred->is_string()) + const auto entry = data.find(serverAddress); + if (entry == data.end() || !entry->is_string()) { return std::nullopt; } - try - { - return Unprotect(cred->get()); - } - catch (...) - { - LOG_CAUGHT_EXCEPTION_MSG("Failed to decrypt credential for %hs", serverAddress.c_str()); - return std::nullopt; - } + return Unprotect(entry->get()); } void RegistryService::FileEraseCredential(const std::string& serverAddress) { bool erased = false; ModifyFileStore([&](nlohmann::json& data) { - auto registries = data.find("registries"); - if (registries == data.end() || !registries->is_object()) - { - return false; - } - - erased = registries->erase(serverAddress) > 0; + erased = data.erase(serverAddress) > 0; return erased; }); THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased); } -std::vector RegistryService::FileListCredentials() +std::vector RegistryService::FileListCredentials() { - std::vector result; + std::vector result; auto data = ReadFileStore(); - const auto registries = data.find("registries"); - if (registries != data.end() && registries->is_object()) + for (const auto& [key, value] : data.items()) { - for (const auto& [key, _] : registries->items()) + if (value.is_string()) { - result.push_back(key); + result.push_back(wsl::shared::string::MultiByteToWide(key)); } } diff --git a/src/windows/wslc/services/RegistryService.h b/src/windows/wslc/services/RegistryService.h index 2e5b694b0..fe4509bb6 100644 --- a/src/windows/wslc/services/RegistryService.h +++ b/src/windows/wslc/services/RegistryService.h @@ -28,7 +28,7 @@ class RegistryService static void Store(const std::string& serverAddress, const std::string& credential); static std::optional Get(const std::string& serverAddress); static void Erase(const std::string& serverAddress); - static std::vector List(); + static std::vector List(); // Authenticates with a registry via the session's Docker engine. // Returns a base64-encoded auth header ready to store and pass to push/pull. @@ -45,10 +45,9 @@ class RegistryService static void WinCredStoreCredential(const std::string& serverAddress, const std::string& credential); static std::optional WinCredGetCredential(const std::string& serverAddress); static void WinCredEraseCredential(const std::string& serverAddress); - static std::vector WinCredListCredentials(); + static std::vector WinCredListCredentials(); // File backend helpers - static std::filesystem::path GetFilePath(); static std::string Protect(const std::string& plaintext); static std::string Unprotect(const std::string& cipherBase64); @@ -58,7 +57,7 @@ class RegistryService static void FileStoreCredential(const std::string& serverAddress, const std::string& credential); static std::optional FileGetCredential(const std::string& serverAddress); static void FileEraseCredential(const std::string& serverAddress); - static std::vector FileListCredentials(); + static std::vector FileListCredentials(); }; } // namespace wsl::windows::wslc::services diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp index 8ad3d879e..467010a55 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.cpp +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.cpp @@ -408,19 +408,10 @@ std::pair StartLocalRegistry(IWSLCSession& se return {std::move(container), std::move(address)}; } -// TODO: Replace with RunWslc("image tag ...") once the 'image tag' CLI command is implemented. -std::string TagImageForRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress) +std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress) { - auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(imageName); - const auto registryImage = std::format("{}/{}:{}", registryAddress, repo, tag.value_or("latest")); - const auto registryRepo = std::format("{}/{}", registryAddress, repo); - - WSLCTagImageOptions tagOptions{}; - tagOptions.Image = imageName.c_str(); - tagOptions.Repo = registryRepo.c_str(); - tagOptions.Tag = tag.value_or("latest").c_str(); - - VERIFY_SUCCEEDED(session.TagImage(&tagOptions)); + auto registryImage = std::format(L"{}/{}", registryAddress, imageName); + RunWslcAndVerify(std::format(L"image tag {} {}", imageName, registryImage), {.ExitCode = 0}); return registryImage; } diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 02d0fc899..7aadef9f5 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -189,8 +189,7 @@ wil::com_ptr OpenDefaultElevatedSession(); std::pair StartLocalRegistry( IWSLCSession& session, const std::string& username = "", const std::string& password = "", USHORT port = 5000); -// TODO: Replace with RunWslc("image tag ...") once the 'image tag' CLI command is implemented. // Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest"). -std::string TagImageForRegistry(IWSLCSession& session, const std::string& imageName, const std::string& registryAddress); +std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress); } // namespace WSLCE2ETests \ No newline at end of file diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index bf7732fed..edfd664bc 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -73,26 +73,25 @@ class WSLCE2EPushPullTests auto registryAddressW = string::MultiByteToWide(registryAddress); // Tag the image for the local registry. - auto registryImage = TagImageForRegistry(*session, "debian:latest", registryAddress); - auto registryImageW = string::MultiByteToWide(registryImage); + auto registryImage = TagImageForRegistry(L"debian:latest", registryAddressW); - auto tagCleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImageW)); }); + auto tagCleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImage)); }); // Push should succeed. - auto result = RunWslc(std::format(L"push {}", registryImageW)); + auto result = RunWslc(std::format(L"push {}", registryImage)); result.Verify({.ExitCode = 0}); // Delete the local copy and pull it back. - RunWslcAndVerify(std::format(L"image delete --force {}", registryImageW), {.ExitCode = 0}); + RunWslcAndVerify(std::format(L"image delete --force {}", registryImage), {.ExitCode = 0}); - result = RunWslc(std::format(L"pull {}", registryImageW)); + result = RunWslc(std::format(L"pull {}", registryImage)); result.Verify({.Stderr = L"", .ExitCode = 0}); // Verify the image is now present. result = RunWslc(L"image list -q"); result.Verify({.Stderr = L"", .ExitCode = 0}); VERIFY_IS_TRUE(result.Stdout.has_value()); - VERIFY_IS_TRUE(result.Stdout->find(registryImageW) != std::wstring::npos); + VERIFY_IS_TRUE(result.Stdout->find(registryImage) != std::wstring::npos); } } diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index 572890d68..e0e6da8ac 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -68,21 +68,20 @@ class WSLCE2ERegistryTests auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15001); auto registryAddressW = string::MultiByteToWide(registryAddress); - auto registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); - auto registryImageNameW = string::MultiByteToWide(registryImageName); + auto registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW); auto cleanup = wil::scope_exit([&]() { - RunWslc(std::format(L"image delete --force {}", registryImageNameW)); + RunWslc(std::format(L"image delete --force {}", registryImageName)); RunWslc(std::format(L"logout {}", registryAddressW)); }); // Negative path before login: push and pull should fail. - auto result = RunWslc(std::format(L"push {}", registryImageNameW)); + auto result = RunWslc(std::format(L"push {}", registryImageName)); VerifyAuthFailure(result); Log::Comment(L"Deleting tagged image and testing pull without login"); - RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); - result = RunWslc(std::format(L"pull {}", registryImageNameW)); + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0}); + result = RunWslc(std::format(L"pull {}", registryImageName)); VerifyAuthFailure(result); // Login and verify that saved credentials are used for push/pull. @@ -90,23 +89,23 @@ class WSLCE2ERegistryTests L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW)); result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0}); - registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); - result = RunWslc(std::format(L"push {}", registryImageNameW)); + registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW); + result = RunWslc(std::format(L"push {}", registryImageName)); result.Verify({.ExitCode = 0}); - RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); - result = RunWslc(std::format(L"pull {}", registryImageNameW)); + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0}); + result = RunWslc(std::format(L"pull {}", registryImageName)); result.Verify({.Stderr = L"", .ExitCode = 0}); // Logout and verify both pull and push fail again. VerifyLogoutSucceeds(registryAddressW); - RunWslcAndVerify(std::format(L"image delete --force {}", registryImageNameW), {.ExitCode = 0}); - result = RunWslc(std::format(L"pull {}", registryImageNameW)); + RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0}); + result = RunWslc(std::format(L"pull {}", registryImageName)); VerifyAuthFailure(result); - registryImageName = TagImageForRegistry(*session, "debian:latest", registryAddress); - result = RunWslc(std::format(L"push {}", registryImageNameW)); + registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW); + result = RunWslc(std::format(L"push {}", registryImageName)); VerifyAuthFailure(result); // Negative path for logout command: second logout should fail. @@ -148,7 +147,7 @@ class WSLCE2ERegistryTests WSLC_TEST_METHOD(WSLCE2E_Registry_Login_CredentialInputMethods) { auto session = OpenDefaultElevatedSession(); - + { auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15002); auto registryAddressW = string::MultiByteToWide(registryAddress); From 100ca147b457a22c7035fe1113f6422dea3c5122 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 08:39:15 -0700 Subject: [PATCH 43/76] Update localization comments --- localization/strings/en-US/Resources.resw | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index db37b688e..7553ba32b 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2315,9 +2315,11 @@ For privacy information about this product please visit https://aka.ms/privacy.< Removing login credentials for {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated Not logged in to {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated Server @@ -2335,11 +2337,11 @@ For privacy information about this product please visit https://aka.ms/privacy.< --password and --password-stdin are mutually exclusive - {Locked="--password"}{Locked="--password-stdin"}CLI flag names should not be translated + {Locked="--password "}{Locked="--password-stdin "}Command line arguments, file names and string inserts should not be translated Must provide --username with --password-stdin - {Locked="--username"}{Locked="--password-stdin"}CLI flag names should not be translated + {Locked="--username "}{Locked="--password-stdin"}Command line arguments, file names and string inserts should not be translated Manage registry credentials. From 83c243c936a639e9fb2c4374c64c5029f818932c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 09:30:39 -0700 Subject: [PATCH 44/76] Address feedback --- src/windows/wslc/commands/RegistryCommand.cpp | 5 ++ .../wslc/services/ContainerService.cpp | 4 +- ...Callback.cpp => ImageProgressCallback.cpp} | 14 ++-- ...mageCallback.h => ImageProgressCallback.h} | 6 +- src/windows/wslc/services/RegistryService.cpp | 83 ++++++++++++------- src/windows/wslc/services/RegistryService.h | 2 +- src/windows/wslc/tasks/ImageTasks.cpp | 6 +- 7 files changed, 73 insertions(+), 47 deletions(-) rename src/windows/wslc/services/{PullImageCallback.cpp => ImageProgressCallback.cpp} (84%) rename src/windows/wslc/services/{PullImageCallback.h => ImageProgressCallback.h} (87%) diff --git a/src/windows/wslc/commands/RegistryCommand.cpp b/src/windows/wslc/commands/RegistryCommand.cpp index 57e939431..688f5e945 100644 --- a/src/windows/wslc/commands/RegistryCommand.cpp +++ b/src/windows/wslc/commands/RegistryCommand.cpp @@ -138,6 +138,11 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const { std::wstring line; std::getline(std::wcin, line); + if (!line.empty() && line.back() == L'\r') + { + line.pop_back(); + } + context.Args.Add(ArgType::Password, std::move(line)); } else diff --git a/src/windows/wslc/services/ContainerService.cpp b/src/windows/wslc/services/ContainerService.cpp index b6da3b35a..60d181e27 100644 --- a/src/windows/wslc/services/ContainerService.cpp +++ b/src/windows/wslc/services/ContainerService.cpp @@ -16,7 +16,7 @@ Module Name: #include "ContainerService.h" #include "ConsoleService.h" #include "ImageService.h" -#include "PullImageCallback.h" +#include "ImageProgressCallback.h" #include #include #include @@ -112,7 +112,7 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Session& sessio { { // Attempt to pull the image if not found - PullImageCallback callback; + ImageProgressCallback callback; PrintMessage(Localization::WSLCCLI_ImageNotFoundPulling(wsl::shared::string::MultiByteToWide(image)), stderr); ImageService imageService; imageService.Pull(session, image, &callback); diff --git a/src/windows/wslc/services/PullImageCallback.cpp b/src/windows/wslc/services/ImageProgressCallback.cpp similarity index 84% rename from src/windows/wslc/services/PullImageCallback.cpp rename to src/windows/wslc/services/ImageProgressCallback.cpp index ebcfd9eb0..412df1096 100644 --- a/src/windows/wslc/services/PullImageCallback.cpp +++ b/src/windows/wslc/services/ImageProgressCallback.cpp @@ -4,16 +4,16 @@ Copyright (c) Microsoft. All rights reserved. Module Name: - PullImageCallback.cpp + ImageProgressCallback.cpp Abstract: - This file contains the PullImageCallback Implementation. + This file contains the ImageProgressCallback Implementation. --*/ #include "precomp.h" -#include "PullImageCallback.h" +#include "ImageProgressCallback.h" #include "ImageService.h" #include @@ -42,7 +42,7 @@ ChangeTerminalMode::~ChangeTerminalMode() } } -auto PullImageCallback::MoveToLine(SHORT line) +auto ImageProgressCallback::MoveToLine(SHORT line) { if (line > 0) { @@ -57,7 +57,7 @@ auto PullImageCallback::MoveToLine(SHORT line) }); } -HRESULT PullImageCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) +HRESULT ImageProgressCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total) { try { @@ -94,14 +94,14 @@ HRESULT PullImageCallback::OnProgress(LPCSTR status, LPCSTR id, ULONGLONG curren CATCH_RETURN(); } -CONSOLE_SCREEN_BUFFER_INFO PullImageCallback::Info() +CONSOLE_SCREEN_BUFFER_INFO ImageProgressCallback::Info() { CONSOLE_SCREEN_BUFFER_INFO info{}; THROW_IF_WIN32_BOOL_FALSE(GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info)); return info; } -std::wstring PullImageCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info) +std::wstring ImageProgressCallback::GenerateStatusLine(LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total, const CONSOLE_SCREEN_BUFFER_INFO& info) { std::wstring line; if (total != 0) diff --git a/src/windows/wslc/services/PullImageCallback.h b/src/windows/wslc/services/ImageProgressCallback.h similarity index 87% rename from src/windows/wslc/services/PullImageCallback.h rename to src/windows/wslc/services/ImageProgressCallback.h index b25ce0717..4a9f7ec74 100644 --- a/src/windows/wslc/services/PullImageCallback.h +++ b/src/windows/wslc/services/ImageProgressCallback.h @@ -4,11 +4,11 @@ Copyright (c) Microsoft. All rights reserved. Module Name: - PullImageCallback.h + ImageProgressCallback.h Abstract: - This file contains the PullImageCallback definition + This file contains the ImageProgressCallback definition --*/ #pragma once @@ -35,7 +35,7 @@ class ChangeTerminalMode }; // TODO: Handle terminal resizes. -class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") PullImageCallback +class DECLSPEC_UUID("7A1D3376-835A-471A-8DC9-23653D9962D0") ImageProgressCallback : public Microsoft::WRL::RuntimeClass, IProgressCallback, IFastRundown> { public: diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index 7efc91877..6496f42a2 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -45,6 +45,18 @@ wil::unique_hfile OpenFileExclusive() { wil::unique_hfile handle( CreateFileW(GetFilePath().c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + THROW_LAST_ERROR_IF(!handle.is_valid() && GetLastError() != ERROR_FILE_NOT_FOUND && GetLastError() != ERROR_PATH_NOT_FOUND); + + return handle; +} + +wil::unique_hfile CreateFileExclusive() +{ + auto filePath = GetFilePath(); + std::filesystem::create_directories(filePath.parent_path()); + + wil::unique_hfile handle( + CreateFileW(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); THROW_LAST_ERROR_IF(!handle.is_valid()); return handle; @@ -54,13 +66,18 @@ wil::unique_hfile OpenFileShared() { wil::unique_hfile handle(CreateFileW( GetFilePath().c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); - THROW_LAST_ERROR_IF(!handle.is_valid() && GetLastError() != ERROR_FILE_NOT_FOUND); + THROW_LAST_ERROR_IF(!handle.is_valid() && GetLastError() != ERROR_FILE_NOT_FOUND && GetLastError() != ERROR_PATH_NOT_FOUND); return handle; } nlohmann::json ReadJsonFile(const wil::unique_hfile& handle) { + if (!handle.is_valid()) + { + return nlohmann::json::object(); + } + LARGE_INTEGER size{}; THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(handle.get(), &size)); if (size.QuadPart == 0) @@ -98,6 +115,17 @@ void WriteJsonFile(const wil::unique_hfile& handle, const nlohmann::json& data) THROW_IF_WIN32_BOOL_FALSE(FlushFileBuffers(handle.get())); } +std::string ResolveCredentialKey(const std::string& serverAddress) +{ + // Normalize known Docker Hub aliases to the canonical DefaultServer key, + // matching Docker CLI's getAuthConfigKey() behavior. + if (serverAddress == "docker.io" || serverAddress == "index.docker.io" || serverAddress == "registry-1.docker.io") + { + return wsl::windows::wslc::services::RegistryService::DefaultServer; + } + + return serverAddress; +} } // namespace namespace wsl::windows::wslc::services { @@ -107,8 +135,9 @@ void RegistryService::Store(const std::string& serverAddress, const std::string& THROW_HR_IF(E_INVALIDARG, serverAddress.empty()); THROW_HR_IF(E_INVALIDARG, credential.empty()); + auto key = ResolveCredentialKey(serverAddress); auto backend = settings::User().Get(); - backend == CredentialStoreType::File ? FileStoreCredential(serverAddress, credential) : WinCredStoreCredential(serverAddress, credential); + backend == CredentialStoreType::File ? FileStoreCredential(key, credential) : WinCredStoreCredential(key, credential); } std::optional RegistryService::Get(const std::string& serverAddress) @@ -118,16 +147,18 @@ std::optional RegistryService::Get(const std::string& serverAddress return std::nullopt; } + auto key = ResolveCredentialKey(serverAddress); auto backend = settings::User().Get(); - return backend == CredentialStoreType::File ? FileGetCredential(serverAddress) : WinCredGetCredential(serverAddress); + return backend == CredentialStoreType::File ? FileGetCredential(key) : WinCredGetCredential(key); } void RegistryService::Erase(const std::string& serverAddress) { THROW_HR_IF(E_INVALIDARG, serverAddress.empty()); + auto key = ResolveCredentialKey(serverAddress); auto backend = settings::User().Get(); - backend == CredentialStoreType::File ? FileEraseCredential(serverAddress) : WinCredEraseCredential(serverAddress); + backend == CredentialStoreType::File ? FileEraseCredential(key) : WinCredEraseCredential(key); } std::vector RegistryService::List() @@ -149,7 +180,7 @@ void RegistryService::WinCredStoreCredential(const std::string& serverAddress, c CREDENTIALW cred{}; cred.Type = CRED_TYPE_GENERIC; - cred.TargetName = targetName.data(); + cred.TargetName = const_cast(targetName.c_str()); cred.CredentialBlobSize = static_cast(credential.size()); cred.CredentialBlob = reinterpret_cast(const_cast(credential.data())); cred.Persist = CRED_PERSIST_LOCAL_MACHINE; @@ -218,20 +249,8 @@ std::vector RegistryService::WinCredListCredentials() // --- File backend --- -nlohmann::json RegistryService::ReadFileStore() +void RegistryService::ModifyFileStore(wil::unique_hfile handle, const std::function& modifier) { - auto handle = RetryOpenFileOnSharingViolation(OpenFileShared); - if (!handle.is_valid()) - { - return nlohmann::json::object(); - } - - return ReadJsonFile(handle); -} - -void RegistryService::ModifyFileStore(const std::function& modifier) -{ - auto handle = RetryOpenFileOnSharingViolation(OpenFileExclusive); WI_VERIFY(handle.is_valid()); auto data = ReadJsonFile(handle); @@ -272,17 +291,9 @@ std::string RegistryService::Unprotect(const std::string& cipherBase64) void RegistryService::FileStoreCredential(const std::string& serverAddress, const std::string& credential) { - auto filePath = GetFilePath(); + auto handle = RetryOpenFileOnSharingViolation(CreateFileExclusive); - if (!std::filesystem::exists(filePath)) - { - std::filesystem::create_directories(filePath.parent_path()); - - wil::unique_hfile credFile(CreateFileW(filePath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr)); - THROW_LAST_ERROR_IF(!credFile.is_valid()); - } - - ModifyFileStore([&](nlohmann::json& data) { + ModifyFileStore(std::move(handle), [&](nlohmann::json& data) { data[serverAddress] = Protect(credential); return true; }); @@ -290,7 +301,8 @@ void RegistryService::FileStoreCredential(const std::string& serverAddress, cons std::optional RegistryService::FileGetCredential(const std::string& serverAddress) { - auto data = ReadFileStore(); + auto handle = RetryOpenFileOnSharingViolation(OpenFileShared); + auto data = ReadJsonFile(handle); const auto entry = data.find(serverAddress); if (entry == data.end() || !entry->is_string()) @@ -303,8 +315,14 @@ std::optional RegistryService::FileGetCredential(const std::string& void RegistryService::FileEraseCredential(const std::string& serverAddress) { + auto handle = RetryOpenFileOnSharingViolation(OpenFileExclusive); + if (!handle.is_valid()) + { + THROW_HR_WITH_USER_ERROR(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress))); + } + bool erased = false; - ModifyFileStore([&](nlohmann::json& data) { + ModifyFileStore(std::move(handle), [&](nlohmann::json& data) { erased = data.erase(serverAddress) > 0; return erased; }); @@ -314,8 +332,11 @@ void RegistryService::FileEraseCredential(const std::string& serverAddress) std::vector RegistryService::FileListCredentials() { + auto handle = RetryOpenFileOnSharingViolation(OpenFileShared); + auto data = ReadJsonFile(handle); + std::vector result; - auto data = ReadFileStore(); + for (const auto& [key, value] : data.items()) { if (value.is_string()) diff --git a/src/windows/wslc/services/RegistryService.h b/src/windows/wslc/services/RegistryService.h index fe4509bb6..08ef313e9 100644 --- a/src/windows/wslc/services/RegistryService.h +++ b/src/windows/wslc/services/RegistryService.h @@ -51,7 +51,7 @@ class RegistryService static std::string Protect(const std::string& plaintext); static std::string Unprotect(const std::string& cipherBase64); - static void ModifyFileStore(const std::function& modifier); + static void ModifyFileStore(wil::unique_hfile handle, const std::function& modifier); static nlohmann::json ReadFileStore(); static void FileStoreCredential(const std::string& serverAddress, const std::string& credential); diff --git a/src/windows/wslc/tasks/ImageTasks.cpp b/src/windows/wslc/tasks/ImageTasks.cpp index 94e86de31..43486e772 100644 --- a/src/windows/wslc/tasks/ImageTasks.cpp +++ b/src/windows/wslc/tasks/ImageTasks.cpp @@ -19,7 +19,7 @@ Module Name: #include "ImageModel.h" #include "ImageService.h" #include "ImageTasks.h" -#include "PullImageCallback.h" +#include "ImageProgressCallback.h" #include "TableOutput.h" #include "Task.h" #include @@ -134,7 +134,7 @@ void PullImage(CLIExecutionContext& context) auto& session = context.Data.Get(); auto& imageId = context.Args.Get(); - PullImageCallback callback; + ImageProgressCallback callback; services::ImageService::Pull(session, WideToMultiByte(imageId), &callback); } @@ -145,7 +145,7 @@ void PushImage(CLIExecutionContext& context) auto& session = context.Data.Get(); auto& imageId = context.Args.Get(); - PullImageCallback callback; + ImageProgressCallback callback; services::ImageService::Push(session, WideToMultiByte(imageId), &callback); } From 3aca683a9efc81bfc37bc5a9c71f0a57405d49a1 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 09:38:26 -0700 Subject: [PATCH 45/76] Fix formatting --- src/windows/wslc/services/RegistryService.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index 6496f42a2..78920c60f 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -55,8 +55,7 @@ wil::unique_hfile CreateFileExclusive() auto filePath = GetFilePath(); std::filesystem::create_directories(filePath.parent_path()); - wil::unique_hfile handle( - CreateFileW(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); + wil::unique_hfile handle(CreateFileW(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); THROW_LAST_ERROR_IF(!handle.is_valid()); return handle; @@ -77,7 +76,7 @@ nlohmann::json ReadJsonFile(const wil::unique_hfile& handle) { return nlohmann::json::object(); } - + LARGE_INTEGER size{}; THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(handle.get(), &size)); if (size.QuadPart == 0) From 379ef46372dd4380835a7cde8320341d645e495c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 13:05:49 -0700 Subject: [PATCH 46/76] ADdress copilot suggestions --- src/windows/common/WSLCContainerLauncher.cpp | 2 +- src/windows/common/WSLCContainerLauncher.h | 1 + src/windows/common/wslutil.h | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index 7b31353e9..01e84be0c 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -11,7 +11,7 @@ Module Name: This file contains the implementation for WSLCContainerLauncher. --*/ - +#include "precomp.h" #include "WSLCContainerLauncher.h" using wsl::windows::common::ClientRunningWSLCProcess; diff --git a/src/windows/common/WSLCContainerLauncher.h b/src/windows/common/WSLCContainerLauncher.h index 900a24c06..4643bc875 100644 --- a/src/windows/common/WSLCContainerLauncher.h +++ b/src/windows/common/WSLCContainerLauncher.h @@ -79,6 +79,7 @@ class WSLCContainerLauncher : private WSLCProcessLauncher void SetDnsSearchDomains(std::vector&& DnsSearchDomains); void SetDnsOptions(std::vector&& DnsOptions); + using WSLCProcessLauncher::FormatResult; using WSLCProcessLauncher::SetUser; using WSLCProcessLauncher::SetWorkingDirectory; diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index a9ddcb9a2..5ae95f0c9 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -333,7 +333,12 @@ winrt::Windows::Management::Deployment::PackageVolume GetSystemVolume(); std::string Base64Encode(const std::string& input); std::string Base64Decode(const std::string& encoded); +// Builds the base64-encoded X-Registry-Auth header value used by Docker APIs +// (PullImage, PushImage, etc.) from the given credentials. std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress); + +// Builds the base64-encoded X-Registry-Auth header value from an identity token +// returned by Authenticate(). std::string BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress); } // namespace wsl::windows::common::wslutil From 3c599b76fd62148c354377324eb9931575b98427 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 13:35:37 -0700 Subject: [PATCH 47/76] fix formatting --- src/windows/common/WSLCContainerLauncher.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index 01e84be0c..53ce52cf3 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -11,7 +11,7 @@ Module Name: This file contains the implementation for WSLCContainerLauncher. --*/ -#include "precomp.h" +#include "precomp.h" #include "WSLCContainerLauncher.h" using wsl::windows::common::ClientRunningWSLCProcess; From b8f28f03d9e7dc3373de6423b47e080770fdb35c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 15:13:56 -0700 Subject: [PATCH 48/76] Fix ordering --- src/windows/wslc/commands/RootCommand.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/windows/wslc/commands/RootCommand.cpp b/src/windows/wslc/commands/RootCommand.cpp index 6604ff6ff..d92c563de 100644 --- a/src/windows/wslc/commands/RootCommand.cpp +++ b/src/windows/wslc/commands/RootCommand.cpp @@ -42,11 +42,11 @@ std::vector> RootCommand::GetCommands() const commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); + commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); - commands.push_back(std::make_unique(FullName())); - commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName())); commands.push_back(std::make_unique(FullName(), true)); commands.push_back(std::make_unique(FullName())); From 0235839085100b5f58152c68e6f8390291cb56f8 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 17:18:38 -0700 Subject: [PATCH 49/76] Try different port --- test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index edfd664bc..a1eca68b1 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -69,7 +69,9 @@ class WSLCE2EPushPullTests auto session = OpenDefaultElevatedSession(); { - auto [registryContainer, registryAddress] = StartLocalRegistry(*session); + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, {}, {}, 50001); + + // Ensure the registry container is cleaned up after the test. auto registryAddressW = string::MultiByteToWide(registryAddress); // Tag the image for the local registry. From d516f508fd6a9e59cf530026d826e7ae8836b0ff Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 17:45:23 -0700 Subject: [PATCH 50/76] Fix formatting --- test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index a1eca68b1..c7cd1cbb1 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -71,7 +71,7 @@ class WSLCE2EPushPullTests { auto [registryContainer, registryAddress] = StartLocalRegistry(*session, {}, {}, 50001); - // Ensure the registry container is cleaned up after the test. + // Ensure the registry container is cleaned up after the test. auto registryAddressW = string::MultiByteToWide(registryAddress); // Tag the image for the local registry. From ae72ade10646b32ac22b853816374a9ffcc41f43 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 17:56:50 -0700 Subject: [PATCH 51/76] Cherry-pick PR #14244 (squashed) - upstream microsoft/WSL#14244 --- CMakeLists.txt | 1 + cloudtest/TestGroup.xml.in | 2 +- src/windows/common/wslutil.cpp | 63 ++++++++++++++++++++ src/windows/common/wslutil.h | 3 + src/windows/service/exe/ServiceMain.cpp | 2 + src/windows/wslc/core/Main.cpp | 2 + src/windows/wslcsession/main.cpp | 2 + src/windows/wslhost/main.cpp | 2 + src/windows/wslinstaller/exe/ServiceMain.cpp | 3 + src/windows/wslrelay/main.cpp | 2 + test/windows/CMakeLists.txt | 1 - test/windows/Common.cpp | 12 ++++ 12 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1400001af..c0bab27b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,6 +286,7 @@ set(COMMON_LINK_LIBRARIES synchronization.lib Bcrypt.lib Crypt32.lib + Dbghelp.lib icu.lib) set(MSI_LINK_LIBRARIES diff --git a/cloudtest/TestGroup.xml.in b/cloudtest/TestGroup.xml.in index efa0f59de..1caf82381 100644 --- a/cloudtest/TestGroup.xml.in +++ b/cloudtest/TestGroup.xml.in @@ -22,6 +22,6 @@ - + diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 386b5095f..23275964d 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -22,6 +22,7 @@ Module Name: #include "ConsoleProgressBar.h" #include "ExecutionContext.h" #include "MsiQuery.h" +#include using winrt::Windows::Foundation::Uri; using winrt::Windows::Management::Deployment::DeploymentOptions; @@ -208,6 +209,8 @@ static const std::map g_contextStrings{ #undef X +DEFINE_ENUM_FLAG_OPERATORS(MINIDUMP_TYPE); + wil::unique_hlocal_string GetWinInetErrorString(HRESULT error) { const wil::unique_hmodule library{LoadLibrary(L"WinInet.dll")}; @@ -261,6 +264,61 @@ constexpr GUID EndianSwap(GUID value) return value; } +static LONG WINAPI OnException(_EXCEPTION_POINTERS* exception) +{ + try + { + static std::atomic handlingException = false; + if (handlingException.exchange(true)) + { + return EXCEPTION_CONTINUE_SEARCH; // Don't keep trying if we crash during exception handling. + } + + // Collect a crash dump if enabled. + auto image = std::filesystem::path(wil::GetModuleFileNameW()).filename(); + + auto lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ); + auto crashFolder = wsl::windows::common::registry::ReadOptionalString(lxssKey.get(), nullptr, c_crashFolderKeyName); + + std::optional dumpPath; + if (crashFolder.has_value()) + { + dumpPath = std::filesystem::path(crashFolder.value()) / std::format(L"{}.{}.dmp", image.native(), GetCurrentProcessId()); + } + + WSL_LOG( + "ProcessCrash", + TraceLoggingValue(image.c_str(), "Process"), + TraceLoggingValue(dumpPath.has_value() ? dumpPath->native().c_str() : L"", "DumpPath")); + + if (!dumpPath.has_value()) + { + return EXCEPTION_CONTINUE_SEARCH; + } + + auto dumpFile = wil::create_new_file(dumpPath->c_str(), GENERIC_WRITE, FILE_SHARE_READ); + THROW_LAST_ERROR_IF(!dumpFile); + + MINIDUMP_EXCEPTION_INFORMATION exceptionInfo{}; + exceptionInfo.ThreadId = GetCurrentThreadId(); + exceptionInfo.ExceptionPointers = exception; + + THROW_IF_WIN32_BOOL_FALSE(MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + dumpFile.get(), + MiniDumpWithDataSegs | MiniDumpWithFullMemory | MiniDumpWithProcessThreadData | MiniDumpWithHandleData | + MiniDumpWithPrivateReadWriteMemory | MiniDumpWithUnloadedModules | MiniDumpWithFullMemoryInfo | + MiniDumpWithThreadInfo | MiniDumpWithTokenInformation | MiniDumpWithPrivateWriteCopyMemory | MiniDumpWithCodeSegs, + &exceptionInfo, + nullptr, + nullptr)); + } + CATCH_LOG(); + + return EXCEPTION_CONTINUE_SEARCH; +} + std::regex BuildImageReferenceRegex() { // See: https://github.com/containers/image/blob/main/docker/reference/regexp.go @@ -301,6 +359,11 @@ void wsl::windows::common::wslutil::CoInitializeSecurity() nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_STATIC_CLOAKING, 0)); } +void wsl::windows::common::wslutil::ConfigureCrashHandler() +{ + AddVectoredExceptionHandler(1, OnException); +} + void wsl::windows::common::wslutil::ConfigureCrt() { // _CALL_REPORTFAULT will cause the process to actually crash instead of just exiting. diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index 5ae95f0c9..6d77b310c 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -42,6 +42,7 @@ inline constexpr GUID WslTerminalNamespace = {0xbe9372fe, 0x59e1, 0x4876, {0xbd, // {2bde4a90-d05f-401c-9492-e40884ead1d8} inline constexpr GUID GeneratedProfilesTerminalNamespace = {0x2bde4a90, 0xd05f, 0x401c, {0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8}}; +inline auto c_crashFolderKeyName = L"CrashDumpFolder"; inline auto c_msixPackageFamilyName = L"MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe"; inline auto c_githubUrlOverrideRegistryValue = L"GitHubUrlOverride"; inline auto c_vhdFileExtension = L".vhd"; @@ -187,6 +188,8 @@ wil::com_ptr CoGetCallContext(); void CoInitializeSecurity(); +void ConfigureCrashHandler(); + void ConfigureCrt(); /// diff --git a/src/windows/service/exe/ServiceMain.cpp b/src/windows/service/exe/ServiceMain.cpp index 059f7d599..617e85ee5 100644 --- a/src/windows/service/exe/ServiceMain.cpp +++ b/src/windows/service/exe/ServiceMain.cpp @@ -169,6 +169,8 @@ try WSL_LOG("Service starting", TraceLoggingLevel(WINEVENT_LEVEL_INFO)); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Don't kill the process on unknown C++ exceptions. wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslc/core/Main.cpp b/src/windows/wslc/core/Main.cpp index 8278130b7..ec9a215f6 100644 --- a/src/windows/wslc/core/Main.cpp +++ b/src/windows/wslc/core/Main.cpp @@ -39,6 +39,8 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + wslutil::SetCrtEncoding(_O_U8TEXT); auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslcsession/main.cpp b/src/windows/wslcsession/main.cpp index d04d10c26..5e70713c2 100644 --- a/src/windows/wslcsession/main.cpp +++ b/src/windows/wslcsession/main.cpp @@ -62,6 +62,8 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit([] { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Don't kill the process on unknown C++ exceptions wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslhost/main.cpp b/src/windows/wslhost/main.cpp index e89dbe7ab..38bf449e7 100644 --- a/src/windows/wslhost/main.cpp +++ b/src/windows/wslhost/main.cpp @@ -167,6 +167,8 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Initialize COM. auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wsl::windows::common::wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslinstaller/exe/ServiceMain.cpp b/src/windows/wslinstaller/exe/ServiceMain.cpp index dbf835897..4565ee719 100644 --- a/src/windows/wslinstaller/exe/ServiceMain.cpp +++ b/src/windows/wslinstaller/exe/ServiceMain.cpp @@ -71,6 +71,9 @@ HRESULT WslInstallerService::OnServiceStarting() wsl::windows::common::wslutil::ConfigureCrt(); WslTraceLoggingInitialize(WslServiceTelemetryProvider, !wsl::shared::OfficialBuild); + + wsl::windows::common::wslutil::ConfigureCrashHandler(); + wsl::windows::common::security::ApplyProcessMitigationPolicies(); return S_OK; diff --git a/src/windows/wslrelay/main.cpp b/src/windows/wslrelay/main.cpp index ea5080be2..74ca79568 100644 --- a/src/windows/wslrelay/main.cpp +++ b/src/windows/wslrelay/main.cpp @@ -58,6 +58,8 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, disableTelemetry); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Ensure that the other end of the pipe has connected if required. if (connectPipe) { diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index ee55f617c..da559506c 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -36,7 +36,6 @@ target_link_libraries(wsltests ${SERVICE_LINK_LIBRARIES} VirtDisk.lib Wer.lib - Dbghelp.lib sfc.lib Crypt32.lib) diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index 948a5f826..f626deea9 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -68,6 +68,7 @@ static std::wstring g_pipelineBuildId; std::wstring g_testDistroPath; std::wstring g_testDataPath; bool g_fastTestRun = false; // True when test.bat was invoked with -f +std::optional> g_dumpKeyChange; std::pair CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa) { @@ -2044,6 +2045,17 @@ Return Value: WEX::TestExecution::RuntimeParameters::TryGetValue(L"WerReport", g_enableWerReport); WEX::TestExecution::RuntimeParameters::TryGetValue(L"LogDmesg", g_LogDmesgAfterEachTest); + bool enableCrashDumpCollection = false; + WEX::TestExecution::RuntimeParameters::TryGetValue(L"CollectCrashDumps", enableCrashDumpCollection); + + if (enableCrashDumpCollection) + { + LogInfo("Enabling crash dump collection. Target: %ls", g_dumpFolder.c_str()); + + g_dumpKeyChange.emplace( + HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::wslutil::c_crashFolderKeyName, g_dumpFolder.c_str()); + } + g_WatchdogTimer = CreateThreadpoolTimer(LxsstuWatchdogTimer, nullptr, nullptr); VERIFY_IS_NOT_NULL(g_WatchdogTimer); From 4f51c374f9b88d5a1355ff41f0f4b2f5ef171e82 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 17:57:51 -0700 Subject: [PATCH 52/76] Use default registry port --- test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index c7cd1cbb1..c36b15f94 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -69,7 +69,7 @@ class WSLCE2EPushPullTests auto session = OpenDefaultElevatedSession(); { - auto [registryContainer, registryAddress] = StartLocalRegistry(*session, {}, {}, 50001); + auto [registryContainer, registryAddress] = StartLocalRegistry(*session); // Ensure the registry container is cleaned up after the test. auto registryAddressW = string::MultiByteToWide(registryAddress); From 9a80823b86a437bfffd854778b683bad320c0756 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 22:03:15 -0700 Subject: [PATCH 53/76] Revert "Cherry-pick PR #14244 (squashed) - upstream microsoft/WSL#14244" This reverts commit ae72ade10646b32ac22b853816374a9ffcc41f43. --- CMakeLists.txt | 1 - cloudtest/TestGroup.xml.in | 2 +- src/windows/common/wslutil.cpp | 63 -------------------- src/windows/common/wslutil.h | 3 - src/windows/service/exe/ServiceMain.cpp | 2 - src/windows/wslc/core/Main.cpp | 2 - src/windows/wslcsession/main.cpp | 2 - src/windows/wslhost/main.cpp | 2 - src/windows/wslinstaller/exe/ServiceMain.cpp | 3 - src/windows/wslrelay/main.cpp | 2 - test/windows/CMakeLists.txt | 1 + test/windows/Common.cpp | 12 ---- 12 files changed, 2 insertions(+), 93 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c0bab27b9..1400001af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,7 +286,6 @@ set(COMMON_LINK_LIBRARIES synchronization.lib Bcrypt.lib Crypt32.lib - Dbghelp.lib icu.lib) set(MSI_LINK_LIBRARIES diff --git a/cloudtest/TestGroup.xml.in b/cloudtest/TestGroup.xml.in index 1caf82381..efa0f59de 100644 --- a/cloudtest/TestGroup.xml.in +++ b/cloudtest/TestGroup.xml.in @@ -22,6 +22,6 @@ - + diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 23275964d..386b5095f 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -22,7 +22,6 @@ Module Name: #include "ConsoleProgressBar.h" #include "ExecutionContext.h" #include "MsiQuery.h" -#include using winrt::Windows::Foundation::Uri; using winrt::Windows::Management::Deployment::DeploymentOptions; @@ -209,8 +208,6 @@ static const std::map g_contextStrings{ #undef X -DEFINE_ENUM_FLAG_OPERATORS(MINIDUMP_TYPE); - wil::unique_hlocal_string GetWinInetErrorString(HRESULT error) { const wil::unique_hmodule library{LoadLibrary(L"WinInet.dll")}; @@ -264,61 +261,6 @@ constexpr GUID EndianSwap(GUID value) return value; } -static LONG WINAPI OnException(_EXCEPTION_POINTERS* exception) -{ - try - { - static std::atomic handlingException = false; - if (handlingException.exchange(true)) - { - return EXCEPTION_CONTINUE_SEARCH; // Don't keep trying if we crash during exception handling. - } - - // Collect a crash dump if enabled. - auto image = std::filesystem::path(wil::GetModuleFileNameW()).filename(); - - auto lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ); - auto crashFolder = wsl::windows::common::registry::ReadOptionalString(lxssKey.get(), nullptr, c_crashFolderKeyName); - - std::optional dumpPath; - if (crashFolder.has_value()) - { - dumpPath = std::filesystem::path(crashFolder.value()) / std::format(L"{}.{}.dmp", image.native(), GetCurrentProcessId()); - } - - WSL_LOG( - "ProcessCrash", - TraceLoggingValue(image.c_str(), "Process"), - TraceLoggingValue(dumpPath.has_value() ? dumpPath->native().c_str() : L"", "DumpPath")); - - if (!dumpPath.has_value()) - { - return EXCEPTION_CONTINUE_SEARCH; - } - - auto dumpFile = wil::create_new_file(dumpPath->c_str(), GENERIC_WRITE, FILE_SHARE_READ); - THROW_LAST_ERROR_IF(!dumpFile); - - MINIDUMP_EXCEPTION_INFORMATION exceptionInfo{}; - exceptionInfo.ThreadId = GetCurrentThreadId(); - exceptionInfo.ExceptionPointers = exception; - - THROW_IF_WIN32_BOOL_FALSE(MiniDumpWriteDump( - GetCurrentProcess(), - GetCurrentProcessId(), - dumpFile.get(), - MiniDumpWithDataSegs | MiniDumpWithFullMemory | MiniDumpWithProcessThreadData | MiniDumpWithHandleData | - MiniDumpWithPrivateReadWriteMemory | MiniDumpWithUnloadedModules | MiniDumpWithFullMemoryInfo | - MiniDumpWithThreadInfo | MiniDumpWithTokenInformation | MiniDumpWithPrivateWriteCopyMemory | MiniDumpWithCodeSegs, - &exceptionInfo, - nullptr, - nullptr)); - } - CATCH_LOG(); - - return EXCEPTION_CONTINUE_SEARCH; -} - std::regex BuildImageReferenceRegex() { // See: https://github.com/containers/image/blob/main/docker/reference/regexp.go @@ -359,11 +301,6 @@ void wsl::windows::common::wslutil::CoInitializeSecurity() nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_STATIC_CLOAKING, 0)); } -void wsl::windows::common::wslutil::ConfigureCrashHandler() -{ - AddVectoredExceptionHandler(1, OnException); -} - void wsl::windows::common::wslutil::ConfigureCrt() { // _CALL_REPORTFAULT will cause the process to actually crash instead of just exiting. diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index 6d77b310c..5ae95f0c9 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -42,7 +42,6 @@ inline constexpr GUID WslTerminalNamespace = {0xbe9372fe, 0x59e1, 0x4876, {0xbd, // {2bde4a90-d05f-401c-9492-e40884ead1d8} inline constexpr GUID GeneratedProfilesTerminalNamespace = {0x2bde4a90, 0xd05f, 0x401c, {0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8}}; -inline auto c_crashFolderKeyName = L"CrashDumpFolder"; inline auto c_msixPackageFamilyName = L"MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe"; inline auto c_githubUrlOverrideRegistryValue = L"GitHubUrlOverride"; inline auto c_vhdFileExtension = L".vhd"; @@ -188,8 +187,6 @@ wil::com_ptr CoGetCallContext(); void CoInitializeSecurity(); -void ConfigureCrashHandler(); - void ConfigureCrt(); /// diff --git a/src/windows/service/exe/ServiceMain.cpp b/src/windows/service/exe/ServiceMain.cpp index 617e85ee5..059f7d599 100644 --- a/src/windows/service/exe/ServiceMain.cpp +++ b/src/windows/service/exe/ServiceMain.cpp @@ -169,8 +169,6 @@ try WSL_LOG("Service starting", TraceLoggingLevel(WINEVENT_LEVEL_INFO)); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Don't kill the process on unknown C++ exceptions. wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslc/core/Main.cpp b/src/windows/wslc/core/Main.cpp index ec9a215f6..8278130b7 100644 --- a/src/windows/wslc/core/Main.cpp +++ b/src/windows/wslc/core/Main.cpp @@ -39,8 +39,6 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - wslutil::SetCrtEncoding(_O_U8TEXT); auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslcsession/main.cpp b/src/windows/wslcsession/main.cpp index 5e70713c2..d04d10c26 100644 --- a/src/windows/wslcsession/main.cpp +++ b/src/windows/wslcsession/main.cpp @@ -62,8 +62,6 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit([] { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Don't kill the process on unknown C++ exceptions wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslhost/main.cpp b/src/windows/wslhost/main.cpp index 38bf449e7..e89dbe7ab 100644 --- a/src/windows/wslhost/main.cpp +++ b/src/windows/wslhost/main.cpp @@ -167,8 +167,6 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Initialize COM. auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wsl::windows::common::wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslinstaller/exe/ServiceMain.cpp b/src/windows/wslinstaller/exe/ServiceMain.cpp index 4565ee719..dbf835897 100644 --- a/src/windows/wslinstaller/exe/ServiceMain.cpp +++ b/src/windows/wslinstaller/exe/ServiceMain.cpp @@ -71,9 +71,6 @@ HRESULT WslInstallerService::OnServiceStarting() wsl::windows::common::wslutil::ConfigureCrt(); WslTraceLoggingInitialize(WslServiceTelemetryProvider, !wsl::shared::OfficialBuild); - - wsl::windows::common::wslutil::ConfigureCrashHandler(); - wsl::windows::common::security::ApplyProcessMitigationPolicies(); return S_OK; diff --git a/src/windows/wslrelay/main.cpp b/src/windows/wslrelay/main.cpp index 74ca79568..ea5080be2 100644 --- a/src/windows/wslrelay/main.cpp +++ b/src/windows/wslrelay/main.cpp @@ -58,8 +58,6 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, disableTelemetry); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Ensure that the other end of the pipe has connected if required. if (connectPipe) { diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index da559506c..ee55f617c 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -36,6 +36,7 @@ target_link_libraries(wsltests ${SERVICE_LINK_LIBRARIES} VirtDisk.lib Wer.lib + Dbghelp.lib sfc.lib Crypt32.lib) diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index f626deea9..948a5f826 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -68,7 +68,6 @@ static std::wstring g_pipelineBuildId; std::wstring g_testDistroPath; std::wstring g_testDataPath; bool g_fastTestRun = false; // True when test.bat was invoked with -f -std::optional> g_dumpKeyChange; std::pair CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa) { @@ -2045,17 +2044,6 @@ Return Value: WEX::TestExecution::RuntimeParameters::TryGetValue(L"WerReport", g_enableWerReport); WEX::TestExecution::RuntimeParameters::TryGetValue(L"LogDmesg", g_LogDmesgAfterEachTest); - bool enableCrashDumpCollection = false; - WEX::TestExecution::RuntimeParameters::TryGetValue(L"CollectCrashDumps", enableCrashDumpCollection); - - if (enableCrashDumpCollection) - { - LogInfo("Enabling crash dump collection. Target: %ls", g_dumpFolder.c_str()); - - g_dumpKeyChange.emplace( - HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::wslutil::c_crashFolderKeyName, g_dumpFolder.c_str()); - } - g_WatchdogTimer = CreateThreadpoolTimer(LxsstuWatchdogTimer, nullptr, nullptr); VERIFY_IS_NOT_NULL(g_WatchdogTimer); From 38d85561f6bf3dc5366583746659c2f74e5e3a91 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 22:36:32 -0700 Subject: [PATCH 54/76] Address feedback --- localization/strings/en-US/Resources.resw | 6 + src/windows/wslc/commands/RegistryCommand.cpp | 6 +- src/windows/wslc/services/FileCredStorage.cpp | 231 ++++++++++++++ src/windows/wslc/services/FileCredStorage.h | 34 ++ .../wslc/services/ICredentialStorage.cpp | 33 ++ .../wslc/services/ICredentialStorage.h | 36 +++ src/windows/wslc/services/ImageService.cpp | 19 +- src/windows/wslc/services/RegistryService.cpp | 295 +----------------- src/windows/wslc/services/RegistryService.h | 29 +- src/windows/wslc/services/WinCredStorage.cpp | 105 +++++++ src/windows/wslc/services/WinCredStorage.h | 32 ++ .../wslc/WSLCCLICredStorageUnitTests.cpp | 165 ++++++++++ .../windows/wslc/WSLCCLISettingsUnitTests.cpp | 18 +- 13 files changed, 676 insertions(+), 333 deletions(-) create mode 100644 src/windows/wslc/services/FileCredStorage.cpp create mode 100644 src/windows/wslc/services/FileCredStorage.h create mode 100644 src/windows/wslc/services/ICredentialStorage.cpp create mode 100644 src/windows/wslc/services/ICredentialStorage.h create mode 100644 src/windows/wslc/services/WinCredStorage.cpp create mode 100644 src/windows/wslc/services/WinCredStorage.h create mode 100644 test/windows/wslc/WSLCCLICredStorageUnitTests.cpp diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index edbca87ce..9fab5437f 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2344,6 +2344,12 @@ For privacy information about this product please visit https://aka.ms/privacy.< Must provide --username with --password-stdin {Locked="--username "}{Locked="--password-stdin"}Command line arguments, file names and string inserts should not be translated + + Username: + + + Password: + Manage registry credentials. diff --git a/src/windows/wslc/commands/RegistryCommand.cpp b/src/windows/wslc/commands/RegistryCommand.cpp index 688f5e945..16a207d7b 100644 --- a/src/windows/wslc/commands/RegistryCommand.cpp +++ b/src/windows/wslc/commands/RegistryCommand.cpp @@ -32,7 +32,7 @@ auto MaskInput() if ((input != INVALID_HANDLE_VALUE) && GetConsoleMode(input, &mode)) { - SetConsoleMode(input, mode & ~ENABLE_ECHO_INPUT); + THROW_IF_WIN32_BOOL_FALSE(SetConsoleMode(input, mode & ~ENABLE_ECHO_INPUT)); return wil::scope_exit(std::function([input, mode] { SetConsoleMode(input, mode); std::wcerr << L'\n'; @@ -128,7 +128,7 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const // Prompt for username if not provided. if (!context.Args.Contains(ArgType::Username)) { - context.Args.Add(ArgType::Username, Prompt(L"Username: ", false)); + context.Args.Add(ArgType::Username, Prompt(Localization::WSLCCLI_LoginUsernamePrompt(), false)); } // Resolve password: --password, --password-stdin, or interactive prompt. @@ -147,7 +147,7 @@ void RegistryLoginCommand::ExecuteInternal(CLIExecutionContext& context) const } else { - context.Args.Add(ArgType::Password, Prompt(L"Password: ", true)); + context.Args.Add(ArgType::Password, Prompt(Localization::WSLCCLI_LoginPasswordPrompt(), true)); } } diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp new file mode 100644 index 000000000..d60eef590 --- /dev/null +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -0,0 +1,231 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + FileCredStorage.cpp + +Abstract: + + DPAPI-encrypted JSON file credential storage implementation. + +--*/ + +#include "precomp.h" +#include "FileCredStorage.h" + +using wsl::shared::Localization; + +using namespace wsl::shared; +using namespace wsl::windows::common::wslutil; + +namespace { + +std::filesystem::path GetFilePath() +{ + return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json"; +} + +wil::unique_file RetryOpenFileOnSharingViolation(const std::function& openFunc) +{ + try + { + return wsl::shared::retry::RetryWithTimeout(openFunc, std::chrono::milliseconds(100), std::chrono::seconds(1), []() { + return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); + }); + } + catch (...) + { + auto result = wil::ResultFromCaughtException(); + auto errorString = wsl::windows::common::wslutil::GetSystemErrorString(result); + THROW_HR_WITH_USER_ERROR(result, Localization::MessageWslcFailedToOpenFile(GetFilePath(), errorString)); + } +} + +wil::unique_file OpenFileExclusive() +{ + wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"r+b", _SH_DENYRW)); + if (f) + { + return f; + } + + auto dosError = _doserrno; + if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND) + { + return nullptr; + } + + THROW_WIN32(dosError); +} + +wil::unique_file CreateFileExclusive() +{ + auto filePath = GetFilePath(); + std::filesystem::create_directories(filePath.parent_path()); + + // Use _wsopen_s with _O_CREAT to atomically create-or-open without truncation. + int fd = -1; + auto err = _wsopen_s(&fd, filePath.c_str(), _O_RDWR | _O_CREAT | _O_BINARY, _SH_DENYRW, _S_IREAD | _S_IWRITE); + THROW_IF_WIN32_ERROR(err); + + wil::unique_file f(_fdopen(fd, "r+b")); + if (!f) + { + _close(fd); + THROW_WIN32(_doserrno); + } + + return f; +} + +wil::unique_file OpenFileShared() +{ + wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"rb", _SH_DENYWR)); + if (f) + { + return f; + } + + auto dosError = _doserrno; + if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND) + { + return nullptr; + } + + THROW_WIN32(dosError); +} + +nlohmann::json ReadJsonFile(FILE* f) +{ + if (!f) + { + return nlohmann::json::object(); + } + + fseek(f, 0, SEEK_SET); + + // Handle newly created empty files (from CreateFileExclusive). + if (_filelengthi64(_fileno(f)) <= 0) + { + return nlohmann::json::object(); + } + + return nlohmann::json::parse(f); +} + +void WriteJsonFile(FILE* f, const nlohmann::json& data) +{ + fseek(f, 0, SEEK_SET); + _chsize_s(_fileno(f), 0); + + auto content = data.dump(2); + fwrite(content.data(), 1, content.size(), f); + fflush(f); +} + +} // namespace + +namespace wsl::windows::wslc::services { + +void FileCredStorage::ModifyFileStore(FILE* f, const std::function& modifier) +{ + WI_VERIFY(f != nullptr); + + auto data = ReadJsonFile(f); + + if (modifier(data)) + { + WriteJsonFile(f, data); + } +} + +std::string FileCredStorage::Protect(const std::string& plaintext) +{ + DATA_BLOB input{}; + input.cbData = static_cast(plaintext.size()); + input.pbData = reinterpret_cast(const_cast(plaintext.data())); + + DATA_BLOB output{}; + THROW_IF_WIN32_BOOL_FALSE(CryptProtectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)); + auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); }); + + return Base64Encode(std::string(reinterpret_cast(output.pbData), output.cbData)); +} + +std::string FileCredStorage::Unprotect(const std::string& cipherBase64) +{ + auto decoded = Base64Decode(cipherBase64); + + DATA_BLOB input{}; + input.cbData = static_cast(decoded.size()); + input.pbData = reinterpret_cast(decoded.data()); + + DATA_BLOB output{}; + THROW_IF_WIN32_BOOL_FALSE(CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)); + auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); }); + + return std::string(reinterpret_cast(output.pbData), output.cbData); +} + +void FileCredStorage::Store(const std::string& serverAddress, const std::string& credential) +{ + auto file = RetryOpenFileOnSharingViolation(CreateFileExclusive); + + ModifyFileStore(file.get(), [&](nlohmann::json& data) { + data[serverAddress] = Protect(credential); + return true; + }); +} + +std::optional FileCredStorage::Get(const std::string& serverAddress) +{ + auto file = RetryOpenFileOnSharingViolation(OpenFileShared); + auto data = ReadJsonFile(file.get()); + + const auto entry = data.find(serverAddress); + if (entry == data.end() || !entry->is_string()) + { + return std::nullopt; + } + + return Unprotect(entry->get()); +} + +void FileCredStorage::Erase(const std::string& serverAddress) +{ + auto file = RetryOpenFileOnSharingViolation(OpenFileExclusive); + if (!file) + { + THROW_HR_WITH_USER_ERROR(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress))); + } + + bool erased = false; + ModifyFileStore(file.get(), [&](nlohmann::json& data) { + erased = data.erase(serverAddress) > 0; + return erased; + }); + + THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased); +} + +std::vector FileCredStorage::List() +{ + auto file = RetryOpenFileOnSharingViolation(OpenFileShared); + auto data = ReadJsonFile(file.get()); + + std::vector result; + + for (const auto& [key, value] : data.items()) + { + if (value.is_string()) + { + result.push_back(wsl::shared::string::MultiByteToWide(key)); + } + } + + return result; +} + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/FileCredStorage.h b/src/windows/wslc/services/FileCredStorage.h new file mode 100644 index 000000000..e7402c12f --- /dev/null +++ b/src/windows/wslc/services/FileCredStorage.h @@ -0,0 +1,34 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + FileCredStorage.h + +Abstract: + + DPAPI-encrypted JSON file credential storage backend. + +--*/ +#pragma once + +#include "ICredentialStorage.h" + +namespace wsl::windows::wslc::services { + +class FileCredStorage final : public ICredentialStorage +{ +public: + void Store(const std::string& serverAddress, const std::string& credential) override; + std::optional Get(const std::string& serverAddress) override; + void Erase(const std::string& serverAddress) override; + std::vector List() override; + +private: + static std::string Protect(const std::string& plaintext); + static std::string Unprotect(const std::string& cipherBase64); + static void ModifyFileStore(FILE* f, const std::function& modifier); +}; + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ICredentialStorage.cpp b/src/windows/wslc/services/ICredentialStorage.cpp new file mode 100644 index 000000000..6cdd5c2c4 --- /dev/null +++ b/src/windows/wslc/services/ICredentialStorage.cpp @@ -0,0 +1,33 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ICredentialStorage.cpp + +Abstract: + + Factory for credential storage backends. + +--*/ + +#include "ICredentialStorage.h" +#include "FileCredStorage.h" +#include "WinCredStorage.h" +#include "WSLCUserSettings.h" + +namespace wsl::windows::wslc::services { + +std::unique_ptr OpenCredentialStorage() +{ + auto backend = settings::User().Get(); + if (backend == settings::CredentialStoreType::File) + { + return std::make_unique(); + } + + return std::make_unique(); +} + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ICredentialStorage.h b/src/windows/wslc/services/ICredentialStorage.h new file mode 100644 index 000000000..07ffe8d98 --- /dev/null +++ b/src/windows/wslc/services/ICredentialStorage.h @@ -0,0 +1,36 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + ICredentialStorage.h + +Abstract: + + Interface for credential storage backends. + +--*/ +#pragma once + +#include +#include +#include + +namespace wsl::windows::wslc::services { + +// Abstract interface for credential storage backends (WinCred, file-based, etc.). +struct ICredentialStorage +{ + virtual ~ICredentialStorage() = default; + + virtual void Store(const std::string& serverAddress, const std::string& credential) = 0; + virtual std::optional Get(const std::string& serverAddress) = 0; + virtual void Erase(const std::string& serverAddress) = 0; + virtual std::vector List() = 0; +}; + +// Returns the credential storage implementation based on user configuration. +std::unique_ptr OpenCredentialStorage(); + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index 3448d2f25..3f6b5de5d 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -66,22 +66,9 @@ wil::unique_hfile ResolveBuildFile(const std::filesystem::path& contextPath) std::string GetServerFromImage(const std::string& image) { - // Extract the registry domain from an image reference. - // Follows the same logic as Docker's splitDockerDomain in distribution/reference: - // If the part before the first '/' contains a '.' or ':', or is "localhost", - // it's treated as a registry domain. Otherwise it's a Docker Hub repo path. - auto slash = image.find('/'); - if (slash != std::string::npos) - { - auto candidate = image.substr(0, slash); - if (candidate.find('.') != std::string::npos || candidate.find(':') != std::string::npos || candidate == "localhost") - { - return candidate; - } - } - - // TODO: Get default server from the daemon's /info through the wslc session - return std::string(wsl::windows::wslc::services::RegistryService::DefaultServer); + auto [repo, tag] = wsl::windows::common::wslutil::ParseImage(image); + auto [server, path] = wsl::windows::common::wslutil::NormalizeRepo(repo); + return server; } } // namespace diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index 78920c60f..81dcf83dd 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -13,107 +13,12 @@ Module Name: --*/ #include "RegistryService.h" -#include -#include -#include #include -#include -using namespace wsl::shared; using namespace wsl::windows::common::wslutil; -static constexpr auto WinCredPrefix = L"wslc-credential/"; - -using unique_credential = wil::unique_any; -using unique_credential_array = wil::unique_any; - namespace { -std::filesystem::path GetFilePath() -{ - return wsl::windows::common::filesystem::GetLocalAppDataPath(nullptr) / L"wslc" / L"registry-credentials.json"; -} - -wil::unique_hfile RetryOpenFileOnSharingViolation(const std::function& openFunc) -{ - return wsl::shared::retry::RetryWithTimeout(openFunc, std::chrono::milliseconds(100), std::chrono::seconds(1), []() { - return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); - }); -} - -wil::unique_hfile OpenFileExclusive() -{ - wil::unique_hfile handle( - CreateFileW(GetFilePath().c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); - THROW_LAST_ERROR_IF(!handle.is_valid() && GetLastError() != ERROR_FILE_NOT_FOUND && GetLastError() != ERROR_PATH_NOT_FOUND); - - return handle; -} - -wil::unique_hfile CreateFileExclusive() -{ - auto filePath = GetFilePath(); - std::filesystem::create_directories(filePath.parent_path()); - - wil::unique_hfile handle(CreateFileW(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)); - THROW_LAST_ERROR_IF(!handle.is_valid()); - - return handle; -} - -wil::unique_hfile OpenFileShared() -{ - wil::unique_hfile handle(CreateFileW( - GetFilePath().c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); - THROW_LAST_ERROR_IF(!handle.is_valid() && GetLastError() != ERROR_FILE_NOT_FOUND && GetLastError() != ERROR_PATH_NOT_FOUND); - - return handle; -} - -nlohmann::json ReadJsonFile(const wil::unique_hfile& handle) -{ - if (!handle.is_valid()) - { - return nlohmann::json::object(); - } - - LARGE_INTEGER size{}; - THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(handle.get(), &size)); - if (size.QuadPart == 0) - { - return nlohmann::json::object(); - } - - LARGE_INTEGER zero{}; - THROW_IF_WIN32_BOOL_FALSE(SetFilePointerEx(handle.get(), zero, nullptr, FILE_BEGIN)); - - std::string buffer(static_cast(size.QuadPart), '\0'); - DWORD bytesRead = 0; - THROW_IF_WIN32_BOOL_FALSE(ReadFile(handle.get(), buffer.data(), static_cast(buffer.size()), &bytesRead, nullptr)); - buffer.resize(bytesRead); - - try - { - return nlohmann::json::parse(buffer); - } - catch (...) - { - return nlohmann::json::object(); - } -} - -void WriteJsonFile(const wil::unique_hfile& handle, const nlohmann::json& data) -{ - LARGE_INTEGER zero{}; - THROW_IF_WIN32_BOOL_FALSE(SetFilePointerEx(handle.get(), zero, nullptr, FILE_BEGIN)); - THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(handle.get())); - - auto content = data.dump(2); - DWORD written = 0; - THROW_IF_WIN32_BOOL_FALSE(WriteFile(handle.get(), content.data(), static_cast(content.size()), &written, nullptr)); - THROW_IF_WIN32_BOOL_FALSE(FlushFileBuffers(handle.get())); -} - std::string ResolveCredentialKey(const std::string& serverAddress) { // Normalize known Docker Hub aliases to the canonical DefaultServer key, @@ -134,9 +39,8 @@ void RegistryService::Store(const std::string& serverAddress, const std::string& THROW_HR_IF(E_INVALIDARG, serverAddress.empty()); THROW_HR_IF(E_INVALIDARG, credential.empty()); - auto key = ResolveCredentialKey(serverAddress); - auto backend = settings::User().Get(); - backend == CredentialStoreType::File ? FileStoreCredential(key, credential) : WinCredStoreCredential(key, credential); + auto storage = OpenCredentialStorage(); + storage->Store(ResolveCredentialKey(serverAddress), credential); } std::optional RegistryService::Get(const std::string& serverAddress) @@ -146,205 +50,22 @@ std::optional RegistryService::Get(const std::string& serverAddress return std::nullopt; } - auto key = ResolveCredentialKey(serverAddress); - auto backend = settings::User().Get(); - return backend == CredentialStoreType::File ? FileGetCredential(key) : WinCredGetCredential(key); + auto storage = OpenCredentialStorage(); + return storage->Get(ResolveCredentialKey(serverAddress)); } void RegistryService::Erase(const std::string& serverAddress) { THROW_HR_IF(E_INVALIDARG, serverAddress.empty()); - auto key = ResolveCredentialKey(serverAddress); - auto backend = settings::User().Get(); - backend == CredentialStoreType::File ? FileEraseCredential(key) : WinCredEraseCredential(key); + auto storage = OpenCredentialStorage(); + storage->Erase(ResolveCredentialKey(serverAddress)); } std::vector RegistryService::List() { - auto backend = settings::User().Get(); - return backend == CredentialStoreType::File ? FileListCredentials() : WinCredListCredentials(); -} - -// --- WinCred backend --- - -std::wstring RegistryService::WinCredTargetName(const std::string& serverAddress) -{ - return std::wstring(WinCredPrefix) + wsl::shared::string::MultiByteToWide(serverAddress); -} - -void RegistryService::WinCredStoreCredential(const std::string& serverAddress, const std::string& credential) -{ - auto targetName = WinCredTargetName(serverAddress); - - CREDENTIALW cred{}; - cred.Type = CRED_TYPE_GENERIC; - cred.TargetName = const_cast(targetName.c_str()); - cred.CredentialBlobSize = static_cast(credential.size()); - cred.CredentialBlob = reinterpret_cast(const_cast(credential.data())); - cred.Persist = CRED_PERSIST_LOCAL_MACHINE; - - THROW_IF_WIN32_BOOL_FALSE(CredWriteW(&cred, 0)); -} - -std::optional RegistryService::WinCredGetCredential(const std::string& serverAddress) -{ - auto targetName = WinCredTargetName(serverAddress); - - unique_credential cred; - if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred)) - { - THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); - return std::nullopt; - } - - if (cred.get()->CredentialBlobSize == 0 || cred.get()->CredentialBlob == nullptr) - { - return std::nullopt; - } - - return std::string(reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize); -} - -void RegistryService::WinCredEraseCredential(const std::string& serverAddress) -{ - auto targetName = WinCredTargetName(serverAddress); - - if (!CredDeleteW(targetName.c_str(), CRED_TYPE_GENERIC, 0)) - { - auto error = GetLastError(); - THROW_HR_WITH_USER_ERROR_IF( - E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), error == ERROR_NOT_FOUND); - - THROW_WIN32(error); - } -} - -std::vector RegistryService::WinCredListCredentials() -{ - auto prefix = std::wstring(WinCredPrefix); - auto filter = prefix + L"*"; - - DWORD count = 0; - unique_credential_array creds; - if (!CredEnumerateW(filter.c_str(), 0, &count, &creds)) - { - THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); - return {}; - } - - std::vector result; - result.reserve(count); - - for (DWORD i = 0; i < count; ++i) - { - // Strip the prefix to return the bare server address. - std::wstring_view name(creds.get()[i]->TargetName); - result.emplace_back(name.substr(prefix.size())); - } - - return result; -} - -// --- File backend --- - -void RegistryService::ModifyFileStore(wil::unique_hfile handle, const std::function& modifier) -{ - WI_VERIFY(handle.is_valid()); - - auto data = ReadJsonFile(handle); - - if (modifier(data)) - { - WriteJsonFile(handle, data); - } -} - -std::string RegistryService::Protect(const std::string& plaintext) -{ - DATA_BLOB input{}; - input.cbData = static_cast(plaintext.size()); - input.pbData = reinterpret_cast(const_cast(plaintext.data())); - - DATA_BLOB output{}; - THROW_IF_WIN32_BOOL_FALSE(CryptProtectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)); - auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); }); - - return Base64Encode(std::string(reinterpret_cast(output.pbData), output.cbData)); -} - -std::string RegistryService::Unprotect(const std::string& cipherBase64) -{ - auto decoded = Base64Decode(cipherBase64); - - DATA_BLOB input{}; - input.cbData = static_cast(decoded.size()); - input.pbData = reinterpret_cast(decoded.data()); - - DATA_BLOB output{}; - THROW_IF_WIN32_BOOL_FALSE(CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &output)); - auto cleanup = wil::scope_exit([&]() { LocalFree(output.pbData); }); - - return std::string(reinterpret_cast(output.pbData), output.cbData); -} - -void RegistryService::FileStoreCredential(const std::string& serverAddress, const std::string& credential) -{ - auto handle = RetryOpenFileOnSharingViolation(CreateFileExclusive); - - ModifyFileStore(std::move(handle), [&](nlohmann::json& data) { - data[serverAddress] = Protect(credential); - return true; - }); -} - -std::optional RegistryService::FileGetCredential(const std::string& serverAddress) -{ - auto handle = RetryOpenFileOnSharingViolation(OpenFileShared); - auto data = ReadJsonFile(handle); - - const auto entry = data.find(serverAddress); - if (entry == data.end() || !entry->is_string()) - { - return std::nullopt; - } - - return Unprotect(entry->get()); -} - -void RegistryService::FileEraseCredential(const std::string& serverAddress) -{ - auto handle = RetryOpenFileOnSharingViolation(OpenFileExclusive); - if (!handle.is_valid()) - { - THROW_HR_WITH_USER_ERROR(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress))); - } - - bool erased = false; - ModifyFileStore(std::move(handle), [&](nlohmann::json& data) { - erased = data.erase(serverAddress) > 0; - return erased; - }); - - THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased); -} - -std::vector RegistryService::FileListCredentials() -{ - auto handle = RetryOpenFileOnSharingViolation(OpenFileShared); - auto data = ReadJsonFile(handle); - - std::vector result; - - for (const auto& [key, value] : data.items()) - { - if (value.is_string()) - { - result.push_back(wsl::shared::string::MultiByteToWide(key)); - } - } - - return result; + auto storage = OpenCredentialStorage(); + return storage->List(); } std::string RegistryService::Authenticate( diff --git a/src/windows/wslc/services/RegistryService.h b/src/windows/wslc/services/RegistryService.h index 08ef313e9..6f7b72b4b 100644 --- a/src/windows/wslc/services/RegistryService.h +++ b/src/windows/wslc/services/RegistryService.h @@ -13,15 +13,13 @@ Module Name: --*/ #pragma once +#include "ICredentialStorage.h" #include "SessionModel.h" -#include "WSLCUserSettings.h" namespace wsl::windows::wslc::services { -using wsl::windows::wslc::settings::CredentialStoreType; - -// Credential store that persists opaque credential strings keyed by server address. -// Supports Windows Credential Manager and DPAPI-encrypted JSON file backends. +// High-level registry authentication service. +// Delegates credential persistence to ICredentialStorage (selected via OpenCredentialStorage). class RegistryService { public: @@ -37,27 +35,6 @@ class RegistryService // Default registry server address used when no explicit server is provided. static constexpr auto DefaultServer = "https://index.docker.io/v1/"; - -private: - static std::wstring WinCredTargetName(const std::string& serverAddress); - - // WinCred helpers - static void WinCredStoreCredential(const std::string& serverAddress, const std::string& credential); - static std::optional WinCredGetCredential(const std::string& serverAddress); - static void WinCredEraseCredential(const std::string& serverAddress); - static std::vector WinCredListCredentials(); - - // File backend helpers - static std::string Protect(const std::string& plaintext); - static std::string Unprotect(const std::string& cipherBase64); - - static void ModifyFileStore(wil::unique_hfile handle, const std::function& modifier); - static nlohmann::json ReadFileStore(); - - static void FileStoreCredential(const std::string& serverAddress, const std::string& credential); - static std::optional FileGetCredential(const std::string& serverAddress); - static void FileEraseCredential(const std::string& serverAddress); - static std::vector FileListCredentials(); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/WinCredStorage.cpp b/src/windows/wslc/services/WinCredStorage.cpp new file mode 100644 index 000000000..13c4a40d5 --- /dev/null +++ b/src/windows/wslc/services/WinCredStorage.cpp @@ -0,0 +1,105 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WinCredStorage.cpp + +Abstract: + + Windows Credential Manager credential storage implementation. + +--*/ + +#include "precomp.h" +#include "WinCredStorage.h" +#include + +using wsl::shared::Localization; + +using unique_credential = wil::unique_any; +using unique_credential_array = wil::unique_any; + +static constexpr auto WinCredPrefix = L"wslc-credential/"; + +namespace wsl::windows::wslc::services { + +std::wstring WinCredStorage::TargetName(const std::string& serverAddress) +{ + return std::wstring(WinCredPrefix) + wsl::shared::string::MultiByteToWide(serverAddress); +} + +void WinCredStorage::Store(const std::string& serverAddress, const std::string& credential) +{ + auto targetName = TargetName(serverAddress); + + CREDENTIALW cred{}; + cred.Type = CRED_TYPE_GENERIC; + cred.TargetName = const_cast(targetName.c_str()); + cred.CredentialBlobSize = static_cast(credential.size()); + cred.CredentialBlob = reinterpret_cast(const_cast(credential.data())); + cred.Persist = CRED_PERSIST_LOCAL_MACHINE; + + THROW_IF_WIN32_BOOL_FALSE(CredWriteW(&cred, 0)); +} + +std::optional WinCredStorage::Get(const std::string& serverAddress) +{ + auto targetName = TargetName(serverAddress); + + unique_credential cred; + if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred)) + { + THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); + return std::nullopt; + } + + if (cred.get()->CredentialBlobSize == 0 || cred.get()->CredentialBlob == nullptr) + { + return std::nullopt; + } + + return std::string(reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize); +} + +void WinCredStorage::Erase(const std::string& serverAddress) +{ + auto targetName = TargetName(serverAddress); + + if (!CredDeleteW(targetName.c_str(), CRED_TYPE_GENERIC, 0)) + { + auto error = GetLastError(); + THROW_HR_WITH_USER_ERROR_IF( + E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), error == ERROR_NOT_FOUND); + + THROW_WIN32(error); + } +} + +std::vector WinCredStorage::List() +{ + auto prefix = std::wstring(WinCredPrefix); + auto filter = prefix + L"*"; + + DWORD count = 0; + unique_credential_array creds; + if (!CredEnumerateW(filter.c_str(), 0, &count, &creds)) + { + THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); + return {}; + } + + std::vector result; + result.reserve(count); + + for (DWORD i = 0; i < count; ++i) + { + std::wstring_view name(creds.get()[i]->TargetName); + result.emplace_back(name.substr(prefix.size())); + } + + return result; +} + +} // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/WinCredStorage.h b/src/windows/wslc/services/WinCredStorage.h new file mode 100644 index 000000000..fe4a68099 --- /dev/null +++ b/src/windows/wslc/services/WinCredStorage.h @@ -0,0 +1,32 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WinCredStorage.h + +Abstract: + + Windows Credential Manager credential storage backend. + +--*/ +#pragma once + +#include "ICredentialStorage.h" + +namespace wsl::windows::wslc::services { + +class WinCredStorage final : public ICredentialStorage +{ +public: + void Store(const std::string& serverAddress, const std::string& credential) override; + std::optional Get(const std::string& serverAddress) override; + void Erase(const std::string& serverAddress) override; + std::vector List() override; + +private: + static std::wstring TargetName(const std::string& serverAddress); +}; + +} // namespace wsl::windows::wslc::services diff --git a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp new file mode 100644 index 000000000..9b482e2c2 --- /dev/null +++ b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp @@ -0,0 +1,165 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + WSLCCredStorageUnitTests.cpp + +Abstract: + + Unit tests for the FileCredStorage and WinCredStorage credential + storage backends. Tests Store, Get, List, and Erase operations. + +--*/ + +#include "precomp.h" +#include "windows/Common.h" +#include "FileCredStorage.h" +#include "WinCredStorage.h" + +using namespace wsl::windows::wslc::services; +using namespace WEX::Logging; +using namespace WEX::Common; +using namespace WEX::TestExecution; + +namespace WSLCCredStorageUnitTests { + +class WSLCCLICredStorageUnitTests +{ + WSL_TEST_CLASS(WSLCCLICredStorageUnitTests) + + TEST_CLASS_SETUP(TestClassSetup) + { + return true; + } + + FileCredStorage m_fileStorage; + WinCredStorage m_winCredStorage; + + static void TestStoreAndGetRoundTrips(ICredentialStorage& storage) + { + auto cleanup = wil::scope_exit([&]() { storage.Erase("wslc-test-server1"); }); + storage.Store("wslc-test-server1", "credential-data-1"); + + auto result = storage.Get("wslc-test-server1"); + VERIFY_IS_TRUE(result.has_value()); + VERIFY_ARE_EQUAL(std::string("credential-data-1"), result.value()); + } + + TEST_METHOD(FileCred_Store_And_Get_RoundTrips) + { + TestStoreAndGetRoundTrips(m_fileStorage); + } + TEST_METHOD(WinCred_Store_And_Get_RoundTrips) + { + TestStoreAndGetRoundTrips(m_winCredStorage); + } + + static void TestGetNonExistentReturnsNullopt(ICredentialStorage& storage) + { + auto result = storage.Get("wslc-test-nonexistent-server"); + VERIFY_IS_FALSE(result.has_value()); + } + + TEST_METHOD(FileCred_Get_NonExistent_ReturnsNullopt) + { + TestGetNonExistentReturnsNullopt(m_fileStorage); + } + TEST_METHOD(WinCred_Get_NonExistent_ReturnsNullopt) + { + TestGetNonExistentReturnsNullopt(m_winCredStorage); + } + + static void TestStoreOverwritesExistingCredential(ICredentialStorage& storage) + { + auto cleanup = wil::scope_exit([&]() { storage.Erase("wslc-test-server2"); }); + storage.Store("wslc-test-server2", "old-credential"); + storage.Store("wslc-test-server2", "new-credential"); + + auto result = storage.Get("wslc-test-server2"); + VERIFY_IS_TRUE(result.has_value()); + VERIFY_ARE_EQUAL(std::string("new-credential"), result.value()); + } + + TEST_METHOD(FileCred_Store_Overwrites_ExistingCredential) + { + TestStoreOverwritesExistingCredential(m_fileStorage); + } + TEST_METHOD(WinCred_Store_Overwrites_ExistingCredential) + { + TestStoreOverwritesExistingCredential(m_winCredStorage); + } + + static void TestListContainsStoredServers(ICredentialStorage& storage) + { + auto cleanup = wil::scope_exit([&]() { + storage.Erase("wslc-test-list1"); + storage.Erase("wslc-test-list2"); + }); + storage.Store("wslc-test-list1", "cred1"); + storage.Store("wslc-test-list2", "cred2"); + + auto servers = storage.List(); + bool found1 = false, found2 = false; + for (const auto& s : servers) + { + if (s == L"wslc-test-list1") + { + found1 = true; + } + if (s == L"wslc-test-list2") + { + found2 = true; + } + } + + VERIFY_IS_TRUE(found1); + VERIFY_IS_TRUE(found2); + } + + TEST_METHOD(FileCred_List_ContainsStoredServers) + { + TestListContainsStoredServers(m_fileStorage); + } + TEST_METHOD(WinCred_List_ContainsStoredServers) + { + TestListContainsStoredServers(m_winCredStorage); + } + + static void TestEraseRemovesCredential(ICredentialStorage& storage) + { + storage.Store("wslc-test-erase", "cred"); + VERIFY_IS_TRUE(storage.Get("wslc-test-erase").has_value()); + + storage.Erase("wslc-test-erase"); + VERIFY_IS_FALSE(storage.Get("wslc-test-erase").has_value()); + } + + TEST_METHOD(FileCred_Erase_RemovesCredential) + { + TestEraseRemovesCredential(m_fileStorage); + } + TEST_METHOD(WinCred_Erase_RemovesCredential) + { + TestEraseRemovesCredential(m_winCredStorage); + } + + static void TestEraseNonExistentThrows(ICredentialStorage& storage) + { + VERIFY_THROWS_SPECIFIC(storage.Erase("wslc-test-nonexistent-erase"), wil::ResultException, [](const wil::ResultException& e) { + return e.GetErrorCode() == E_NOT_SET; + }); + } + + TEST_METHOD(FileCred_Erase_NonExistent_Throws) + { + TestEraseNonExistentThrows(m_fileStorage); + } + TEST_METHOD(WinCred_Erase_NonExistent_Throws) + { + TestEraseNonExistentThrows(m_winCredStorage); + } +}; + +} // namespace WSLCCredStorageUnitTests diff --git a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp index e7152e89a..e2dcc7a23 100644 --- a/test/windows/wslc/WSLCCLISettingsUnitTests.cpp +++ b/test/windows/wslc/WSLCCLISettingsUnitTests.cpp @@ -95,6 +95,7 @@ class WSLCCLISettingsUnitTests VERIFY_ARE_EQUAL(2048u, map.GetOrDefault()); VERIFY_ARE_EQUAL(102400u, map.GetOrDefault()); VERIFY_ARE_EQUAL(std::wstring{}, map.GetOrDefault()); + VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::WinCred), static_cast(map.GetOrDefault())); } // After inserting a value, GetOrDefault must return it rather than the default. @@ -122,6 +123,7 @@ class WSLCCLISettingsUnitTests VERIFY_ARE_EQUAL(2048u, s.Get()); VERIFY_ARE_EQUAL(102400u, s.Get()); VERIFY_ARE_EQUAL(std::wstring{}, s.Get()); + VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::WinCred), static_cast(s.Get())); } // ----------------------------------------------------------------------- @@ -138,7 +140,8 @@ class WSLCCLISettingsUnitTests "session:\n" " cpuCount: 8\n" " memorySize: 4GB\n" - " maxStorageSize: 20000MB\n"); + " maxStorageSize: 20000MB\n" + "credentialStore: file\n"); UserSettingsTest s{dir}; @@ -149,6 +152,7 @@ class WSLCCLISettingsUnitTests VERIFY_ARE_EQUAL(20000u, s.Get()); // Unspecified setting falls back to built-in default. VERIFY_ARE_EQUAL(std::wstring{}, s.Get()); + VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::File), static_cast(s.Get())); } // An empty settings file is valid YAML (null document); all settings use @@ -272,6 +276,18 @@ class WSLCCLISettingsUnitTests VERIFY_ARE_EQUAL(std::wstring{}, s.Get()); } + // credentialStore: invalid value must fall back to default and warn. + TEST_METHOD(Validation_CredentialStore_Invalid_UsesDefaultAndWarns) + { + auto dir = UniqueTempDir(); + WriteFile(dir / L"settings.yaml", "credentialStore: badvalue\n"); + + UserSettingsTest s{dir}; + + VERIFY_ARE_EQUAL(static_cast(CredentialStoreType::WinCred), static_cast(s.Get())); + VERIFY_IS_TRUE(s.GetWarnings().size() >= 1u); + } + // Extra unknown keys at any level must not cause errors or warnings. TEST_METHOD(Validation_UnknownKeys_NoErrorsOrWarnings) { From 609bd82206cb94773b8ad9085b7b58938f941496 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 22:40:25 -0700 Subject: [PATCH 55/76] Remove unnecessary code --- test/windows/wslc/WSLCCLICredStorageUnitTests.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp index 9b482e2c2..319095674 100644 --- a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp +++ b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp @@ -29,11 +29,6 @@ class WSLCCLICredStorageUnitTests { WSL_TEST_CLASS(WSLCCLICredStorageUnitTests) - TEST_CLASS_SETUP(TestClassSetup) - { - return true; - } - FileCredStorage m_fileStorage; WinCredStorage m_winCredStorage; From 132687a6fa7dab9a4d1f7b8db3ea472b1da56a5e Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 22:41:34 -0700 Subject: [PATCH 56/76] fix formatting --- test/windows/wslc/WSLCCLICredStorageUnitTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp index 319095674..7b890170f 100644 --- a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp +++ b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp @@ -28,7 +28,7 @@ namespace WSLCCredStorageUnitTests { class WSLCCLICredStorageUnitTests { WSL_TEST_CLASS(WSLCCLICredStorageUnitTests) - + FileCredStorage m_fileStorage; WinCredStorage m_winCredStorage; From dd19ee2a0a6058b2707c1848531b6f7360a0652d Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 22:54:16 -0700 Subject: [PATCH 57/76] Address copilot feedback --- localization/strings/en-US/Resources.resw | 4 ++++ src/windows/wslc/services/FileCredStorage.cpp | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 9fab5437f..cf61837ea 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2322,6 +2322,10 @@ For privacy information about this product please visit https://aka.ms/privacy.< Not logged in to {} {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Failed to parse credentials file '{}': the file may be corrupted. + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Server diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index d60eef590..5c5206480 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -112,7 +112,14 @@ nlohmann::json ReadJsonFile(FILE* f) return nlohmann::json::object(); } - return nlohmann::json::parse(f); + try + { + return nlohmann::json::parse(f); + } + catch (const nlohmann::json::parse_error&) + { + THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON,Localization::WSLCCLI_CredentialFileCorrupt(GetFilePath())); + } } void WriteJsonFile(FILE* f, const nlohmann::json& data) From 618aab9b3e4f3ab3cbb94fd645fb554acb59af7c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 23:07:51 -0700 Subject: [PATCH 58/76] fix formatting --- src/windows/wslc/services/FileCredStorage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index 5c5206480..369837713 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -118,7 +118,7 @@ nlohmann::json ReadJsonFile(FILE* f) } catch (const nlohmann::json::parse_error&) { - THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON,Localization::WSLCCLI_CredentialFileCorrupt(GetFilePath())); + THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON, Localization::WSLCCLI_CredentialFileCorrupt(GetFilePath())); } } From 1ef5fe23447585575c5c5c741a1ba8abf5c05061 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Tue, 14 Apr 2026 23:57:44 -0700 Subject: [PATCH 59/76] Improve credential key parsng --- src/windows/wslc/services/RegistryService.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index 81dcf83dd..db8aab1c6 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -21,14 +21,27 @@ namespace { std::string ResolveCredentialKey(const std::string& serverAddress) { - // Normalize known Docker Hub aliases to the canonical DefaultServer key, - // matching Docker CLI's getAuthConfigKey() behavior. - if (serverAddress == "docker.io" || serverAddress == "index.docker.io" || serverAddress == "registry-1.docker.io") + auto input = serverAddress; + + // Strip scheme + if (auto pos = input.find("://"); pos != std::string::npos) + { + input = input.substr(pos + 3); + } + + // Strip path + if (auto pos = input.find('/'); pos != std::string::npos) + { + input = input.substr(0, pos); + } + + // Map Docker Hub aliases to canonical key. + if (input == "docker.io" || input == "index.docker.io") { return wsl::windows::wslc::services::RegistryService::DefaultServer; } - return serverAddress; + return input; } } // namespace From 8ba43afc020b003e9f523726d563ef917c8cd274 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 00:01:39 -0700 Subject: [PATCH 60/76] Try a new port number --- test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index c36b15f94..9869edd2a 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -69,7 +69,7 @@ class WSLCE2EPushPullTests auto session = OpenDefaultElevatedSession(); { - auto [registryContainer, registryAddress] = StartLocalRegistry(*session); + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, "", "", 15002); // Ensure the registry container is cleaned up after the test. auto registryAddressW = string::MultiByteToWide(registryAddress); From 5b2ed43ab55e99362cbff19baeabb2f6cc493c51 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 10:20:42 -0700 Subject: [PATCH 61/76] Address feedback --- src/windows/wslc/services/FileCredStorage.cpp | 14 +++++++------- src/windows/wslc/services/FileCredStorage.h | 5 ----- src/windows/wslc/services/ICredentialStorage.h | 1 + test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 12 +++++------- test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 4 ++-- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index 369837713..45f0c5a05 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -132,11 +132,7 @@ void WriteJsonFile(FILE* f, const nlohmann::json& data) fflush(f); } -} // namespace - -namespace wsl::windows::wslc::services { - -void FileCredStorage::ModifyFileStore(FILE* f, const std::function& modifier) +void ModifyFileStore(FILE* f, const std::function& modifier) { WI_VERIFY(f != nullptr); @@ -148,7 +144,7 @@ void FileCredStorage::ModifyFileStore(FILE* f, const std::function(plaintext.size()); @@ -161,7 +157,7 @@ std::string FileCredStorage::Protect(const std::string& plaintext) return Base64Encode(std::string(reinterpret_cast(output.pbData), output.cbData)); } -std::string FileCredStorage::Unprotect(const std::string& cipherBase64) +std::string Unprotect(const std::string& cipherBase64) { auto decoded = Base64Decode(cipherBase64); @@ -176,6 +172,10 @@ std::string FileCredStorage::Unprotect(const std::string& cipherBase64) return std::string(reinterpret_cast(output.pbData), output.cbData); } +} // namespace + +namespace wsl::windows::wslc::services { + void FileCredStorage::Store(const std::string& serverAddress, const std::string& credential) { auto file = RetryOpenFileOnSharingViolation(CreateFileExclusive); diff --git a/src/windows/wslc/services/FileCredStorage.h b/src/windows/wslc/services/FileCredStorage.h index e7402c12f..03bf511a9 100644 --- a/src/windows/wslc/services/FileCredStorage.h +++ b/src/windows/wslc/services/FileCredStorage.h @@ -24,11 +24,6 @@ class FileCredStorage final : public ICredentialStorage std::optional Get(const std::string& serverAddress) override; void Erase(const std::string& serverAddress) override; std::vector List() override; - -private: - static std::string Protect(const std::string& plaintext); - static std::string Unprotect(const std::string& cipherBase64); - static void ModifyFileStore(FILE* f, const std::function& modifier); }; } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/ICredentialStorage.h b/src/windows/wslc/services/ICredentialStorage.h index 07ffe8d98..85114a4f9 100644 --- a/src/windows/wslc/services/ICredentialStorage.h +++ b/src/windows/wslc/services/ICredentialStorage.h @@ -13,6 +13,7 @@ Module Name: --*/ #pragma once +#include #include #include #include diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index 9869edd2a..0bd4b0d19 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -59,23 +59,21 @@ class WSLCE2EPushPullTests WSLC_TEST_METHOD(WSLCE2E_Image_PushPull) { - // Ensure the default elevated session exists. - RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0}); - const auto& debianImage = DebianTestImage(); EnsureImageIsLoaded(debianImage); + // Ensure the default elevated session exists. + RunWslcAndVerify(L"container list", {.Stderr = L"", .ExitCode = 0}); + // Start a local registry without auth. auto session = OpenDefaultElevatedSession(); { - auto [registryContainer, registryAddress] = StartLocalRegistry(*session, "", "", 15002); - - // Ensure the registry container is cleaned up after the test. + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, "", "", 15003); auto registryAddressW = string::MultiByteToWide(registryAddress); // Tag the image for the local registry. - auto registryImage = TagImageForRegistry(L"debian:latest", registryAddressW); + auto registryImage = TagImageForRegistry(debianImage.NameAndTag(), registryAddressW); auto tagCleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImage)); }); diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index e0e6da8ac..5403d7a2f 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -68,7 +68,7 @@ class WSLCE2ERegistryTests auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15001); auto registryAddressW = string::MultiByteToWide(registryAddress); - auto registryImageName = TagImageForRegistry(L"debian:latest", registryAddressW); + auto registryImageName = TagImageForRegistry(debianImage.NameAndTag(), registryAddressW); auto cleanup = wil::scope_exit([&]() { RunWslc(std::format(L"image delete --force {}", registryImageName)); @@ -79,8 +79,8 @@ class WSLCE2ERegistryTests auto result = RunWslc(std::format(L"push {}", registryImageName)); VerifyAuthFailure(result); - Log::Comment(L"Deleting tagged image and testing pull without login"); RunWslcAndVerify(std::format(L"image delete --force {}", registryImageName), {.ExitCode = 0}); + result = RunWslc(std::format(L"pull {}", registryImageName)); VerifyAuthFailure(result); From 819d2a2a820bad6924af8d1269d89ca445ba82af Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 11:59:44 -0700 Subject: [PATCH 62/76] Address copilot --- src/windows/wslc/services/FileCredStorage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index 45f0c5a05..bb72065cb 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -68,7 +68,7 @@ wil::unique_file CreateFileExclusive() // Use _wsopen_s with _O_CREAT to atomically create-or-open without truncation. int fd = -1; auto err = _wsopen_s(&fd, filePath.c_str(), _O_RDWR | _O_CREAT | _O_BINARY, _SH_DENYRW, _S_IREAD | _S_IWRITE); - THROW_IF_WIN32_ERROR(err); + THROW_WIN32_IF(_doserrno, err != 0); wil::unique_file f(_fdopen(fd, "r+b")); if (!f) From 3a2f454633dd9ab6b93cf4167cc30d1941bca267 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 12:24:56 -0700 Subject: [PATCH 63/76] TEMP: Cherry-pick PR #14244 crash dump handler (to be reverted) --- CMakeLists.txt | 1 + cloudtest/TestGroup.xml.in | 2 +- src/windows/common/wslutil.cpp | 63 ++++++++++++++++++++ src/windows/common/wslutil.h | 3 + src/windows/service/exe/ServiceMain.cpp | 2 + src/windows/wslc/core/Main.cpp | 2 + src/windows/wslcsession/main.cpp | 2 + src/windows/wslhost/main.cpp | 2 + src/windows/wslinstaller/exe/ServiceMain.cpp | 3 + src/windows/wslrelay/main.cpp | 2 + test/windows/CMakeLists.txt | 1 - test/windows/Common.cpp | 12 ++++ 12 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1400001af..c0bab27b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,6 +286,7 @@ set(COMMON_LINK_LIBRARIES synchronization.lib Bcrypt.lib Crypt32.lib + Dbghelp.lib icu.lib) set(MSI_LINK_LIBRARIES diff --git a/cloudtest/TestGroup.xml.in b/cloudtest/TestGroup.xml.in index efa0f59de..1caf82381 100644 --- a/cloudtest/TestGroup.xml.in +++ b/cloudtest/TestGroup.xml.in @@ -22,6 +22,6 @@ - + diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 386b5095f..23275964d 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -22,6 +22,7 @@ Module Name: #include "ConsoleProgressBar.h" #include "ExecutionContext.h" #include "MsiQuery.h" +#include using winrt::Windows::Foundation::Uri; using winrt::Windows::Management::Deployment::DeploymentOptions; @@ -208,6 +209,8 @@ static const std::map g_contextStrings{ #undef X +DEFINE_ENUM_FLAG_OPERATORS(MINIDUMP_TYPE); + wil::unique_hlocal_string GetWinInetErrorString(HRESULT error) { const wil::unique_hmodule library{LoadLibrary(L"WinInet.dll")}; @@ -261,6 +264,61 @@ constexpr GUID EndianSwap(GUID value) return value; } +static LONG WINAPI OnException(_EXCEPTION_POINTERS* exception) +{ + try + { + static std::atomic handlingException = false; + if (handlingException.exchange(true)) + { + return EXCEPTION_CONTINUE_SEARCH; // Don't keep trying if we crash during exception handling. + } + + // Collect a crash dump if enabled. + auto image = std::filesystem::path(wil::GetModuleFileNameW()).filename(); + + auto lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ); + auto crashFolder = wsl::windows::common::registry::ReadOptionalString(lxssKey.get(), nullptr, c_crashFolderKeyName); + + std::optional dumpPath; + if (crashFolder.has_value()) + { + dumpPath = std::filesystem::path(crashFolder.value()) / std::format(L"{}.{}.dmp", image.native(), GetCurrentProcessId()); + } + + WSL_LOG( + "ProcessCrash", + TraceLoggingValue(image.c_str(), "Process"), + TraceLoggingValue(dumpPath.has_value() ? dumpPath->native().c_str() : L"", "DumpPath")); + + if (!dumpPath.has_value()) + { + return EXCEPTION_CONTINUE_SEARCH; + } + + auto dumpFile = wil::create_new_file(dumpPath->c_str(), GENERIC_WRITE, FILE_SHARE_READ); + THROW_LAST_ERROR_IF(!dumpFile); + + MINIDUMP_EXCEPTION_INFORMATION exceptionInfo{}; + exceptionInfo.ThreadId = GetCurrentThreadId(); + exceptionInfo.ExceptionPointers = exception; + + THROW_IF_WIN32_BOOL_FALSE(MiniDumpWriteDump( + GetCurrentProcess(), + GetCurrentProcessId(), + dumpFile.get(), + MiniDumpWithDataSegs | MiniDumpWithFullMemory | MiniDumpWithProcessThreadData | MiniDumpWithHandleData | + MiniDumpWithPrivateReadWriteMemory | MiniDumpWithUnloadedModules | MiniDumpWithFullMemoryInfo | + MiniDumpWithThreadInfo | MiniDumpWithTokenInformation | MiniDumpWithPrivateWriteCopyMemory | MiniDumpWithCodeSegs, + &exceptionInfo, + nullptr, + nullptr)); + } + CATCH_LOG(); + + return EXCEPTION_CONTINUE_SEARCH; +} + std::regex BuildImageReferenceRegex() { // See: https://github.com/containers/image/blob/main/docker/reference/regexp.go @@ -301,6 +359,11 @@ void wsl::windows::common::wslutil::CoInitializeSecurity() nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_STATIC_CLOAKING, 0)); } +void wsl::windows::common::wslutil::ConfigureCrashHandler() +{ + AddVectoredExceptionHandler(1, OnException); +} + void wsl::windows::common::wslutil::ConfigureCrt() { // _CALL_REPORTFAULT will cause the process to actually crash instead of just exiting. diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index 5ae95f0c9..6d77b310c 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -42,6 +42,7 @@ inline constexpr GUID WslTerminalNamespace = {0xbe9372fe, 0x59e1, 0x4876, {0xbd, // {2bde4a90-d05f-401c-9492-e40884ead1d8} inline constexpr GUID GeneratedProfilesTerminalNamespace = {0x2bde4a90, 0xd05f, 0x401c, {0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8}}; +inline auto c_crashFolderKeyName = L"CrashDumpFolder"; inline auto c_msixPackageFamilyName = L"MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe"; inline auto c_githubUrlOverrideRegistryValue = L"GitHubUrlOverride"; inline auto c_vhdFileExtension = L".vhd"; @@ -187,6 +188,8 @@ wil::com_ptr CoGetCallContext(); void CoInitializeSecurity(); +void ConfigureCrashHandler(); + void ConfigureCrt(); /// diff --git a/src/windows/service/exe/ServiceMain.cpp b/src/windows/service/exe/ServiceMain.cpp index 059f7d599..617e85ee5 100644 --- a/src/windows/service/exe/ServiceMain.cpp +++ b/src/windows/service/exe/ServiceMain.cpp @@ -169,6 +169,8 @@ try WSL_LOG("Service starting", TraceLoggingLevel(WINEVENT_LEVEL_INFO)); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Don't kill the process on unknown C++ exceptions. wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslc/core/Main.cpp b/src/windows/wslc/core/Main.cpp index 8278130b7..ec9a215f6 100644 --- a/src/windows/wslc/core/Main.cpp +++ b/src/windows/wslc/core/Main.cpp @@ -39,6 +39,8 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + wslutil::SetCrtEncoding(_O_U8TEXT); auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslcsession/main.cpp b/src/windows/wslcsession/main.cpp index d04d10c26..5e70713c2 100644 --- a/src/windows/wslcsession/main.cpp +++ b/src/windows/wslcsession/main.cpp @@ -62,6 +62,8 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit([] { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Don't kill the process on unknown C++ exceptions wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslhost/main.cpp b/src/windows/wslhost/main.cpp index e89dbe7ab..38bf449e7 100644 --- a/src/windows/wslhost/main.cpp +++ b/src/windows/wslhost/main.cpp @@ -167,6 +167,8 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Initialize COM. auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wsl::windows::common::wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslinstaller/exe/ServiceMain.cpp b/src/windows/wslinstaller/exe/ServiceMain.cpp index dbf835897..4565ee719 100644 --- a/src/windows/wslinstaller/exe/ServiceMain.cpp +++ b/src/windows/wslinstaller/exe/ServiceMain.cpp @@ -71,6 +71,9 @@ HRESULT WslInstallerService::OnServiceStarting() wsl::windows::common::wslutil::ConfigureCrt(); WslTraceLoggingInitialize(WslServiceTelemetryProvider, !wsl::shared::OfficialBuild); + + wsl::windows::common::wslutil::ConfigureCrashHandler(); + wsl::windows::common::security::ApplyProcessMitigationPolicies(); return S_OK; diff --git a/src/windows/wslrelay/main.cpp b/src/windows/wslrelay/main.cpp index ea5080be2..74ca79568 100644 --- a/src/windows/wslrelay/main.cpp +++ b/src/windows/wslrelay/main.cpp @@ -58,6 +58,8 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, disableTelemetry); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); + wsl::windows::common::wslutil::ConfigureCrashHandler(); + // Ensure that the other end of the pipe has connected if required. if (connectPipe) { diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index ee55f617c..da559506c 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -36,7 +36,6 @@ target_link_libraries(wsltests ${SERVICE_LINK_LIBRARIES} VirtDisk.lib Wer.lib - Dbghelp.lib sfc.lib Crypt32.lib) diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index 948a5f826..f626deea9 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -68,6 +68,7 @@ static std::wstring g_pipelineBuildId; std::wstring g_testDistroPath; std::wstring g_testDataPath; bool g_fastTestRun = false; // True when test.bat was invoked with -f +std::optional> g_dumpKeyChange; std::pair CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa) { @@ -2044,6 +2045,17 @@ Return Value: WEX::TestExecution::RuntimeParameters::TryGetValue(L"WerReport", g_enableWerReport); WEX::TestExecution::RuntimeParameters::TryGetValue(L"LogDmesg", g_LogDmesgAfterEachTest); + bool enableCrashDumpCollection = false; + WEX::TestExecution::RuntimeParameters::TryGetValue(L"CollectCrashDumps", enableCrashDumpCollection); + + if (enableCrashDumpCollection) + { + LogInfo("Enabling crash dump collection. Target: %ls", g_dumpFolder.c_str()); + + g_dumpKeyChange.emplace( + HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::wslutil::c_crashFolderKeyName, g_dumpFolder.c_str()); + } + g_WatchdogTimer = CreateThreadpoolTimer(LxsstuWatchdogTimer, nullptr, nullptr); VERIFY_IS_NOT_NULL(g_WatchdogTimer); From f94022efe177d410d39b41b5f61debdef8445c10 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 13:30:46 -0700 Subject: [PATCH 64/76] Set Crash dump handler in wslc cloud test --- cloudtest/TestGroup-wslc.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudtest/TestGroup-wslc.xml.in b/cloudtest/TestGroup-wslc.xml.in index 888ff604c..c8f05abbd 100644 --- a/cloudtest/TestGroup-wslc.xml.in +++ b/cloudtest/TestGroup-wslc.xml.in @@ -22,6 +22,6 @@ - + From 53c7f635f880d00aaf0dfb2fde963ee771564e70 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 15:16:10 -0700 Subject: [PATCH 65/76] Catch excpetions in test host process --- src/windows/common/wslutil.cpp | 2 +- test/windows/Common.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 23275964d..59160c00d 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -361,7 +361,7 @@ void wsl::windows::common::wslutil::CoInitializeSecurity() void wsl::windows::common::wslutil::ConfigureCrashHandler() { - AddVectoredExceptionHandler(1, OnException); + SetUnhandledExceptionFilter(OnException); } void wsl::windows::common::wslutil::ConfigureCrt() diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index f626deea9..7d526f54c 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -2054,6 +2054,9 @@ Return Value: g_dumpKeyChange.emplace( HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::wslutil::c_crashFolderKeyName, g_dumpFolder.c_str()); + + // Also install the vectored exception handler in the test host process itself. + wsl::windows::common::wslutil::ConfigureCrashHandler(); } g_WatchdogTimer = CreateThreadpoolTimer(LxsstuWatchdogTimer, nullptr, nullptr); From 15d396264744343629c33e3a507f01af02409af9 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 15:16:25 -0700 Subject: [PATCH 66/76] Address feedback --- src/windows/wslc/services/FileCredStorage.cpp | 91 ++++++++++--------- src/windows/wslc/services/FileCredStorage.h | 17 ++++ .../windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 35 +++++++ 3 files changed, 102 insertions(+), 41 deletions(-) diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index bb72065cb..a6e486d7a 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -19,6 +19,7 @@ using wsl::shared::Localization; using namespace wsl::shared; using namespace wsl::windows::common::wslutil; +using namespace wsl::windows::wslc::services; namespace { @@ -97,50 +98,52 @@ wil::unique_file OpenFileShared() THROW_WIN32(dosError); } -nlohmann::json ReadJsonFile(FILE* f) +CredentialFile ReadCredentialFile(FILE* f) { - if (!f) - { - return nlohmann::json::object(); - } + WI_ASSERT(f != nullptr); - fseek(f, 0, SEEK_SET); + auto seekResult = fseek(f, 0, SEEK_SET); + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), seekResult != 0); // Handle newly created empty files (from CreateFileExclusive). if (_filelengthi64(_fileno(f)) <= 0) { - return nlohmann::json::object(); + return {}; } try { - return nlohmann::json::parse(f); + return nlohmann::json::parse(f).get(); } - catch (const nlohmann::json::parse_error&) + catch (const nlohmann::json::exception&) { THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_JSON, Localization::WSLCCLI_CredentialFileCorrupt(GetFilePath())); } } -void WriteJsonFile(FILE* f, const nlohmann::json& data) +void WriteCredentialFile(FILE* f, const CredentialFile& data) { - fseek(f, 0, SEEK_SET); - _chsize_s(_fileno(f), 0); - - auto content = data.dump(2); - fwrite(content.data(), 1, content.size(), f); - fflush(f); + auto error = fseek(f, 0, SEEK_SET); + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), error != 0); + + error = _chsize_s(_fileno(f), 0); + THROW_HR_WITH_USER_ERROR_IF( + HRESULT_FROM_WIN32(_doserrno), + Localization::MessageWslcFailedToOpenFile(GetFilePath(), GetSystemErrorString(HRESULT_FROM_WIN32(_doserrno))), + error != 0); + + auto content = nlohmann::json(data).dump(2); + auto written = fwrite(content.data(), 1, content.size(), f); + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), written != content.size()); } -void ModifyFileStore(FILE* f, const std::function& modifier) +void ModifyFileStore(FILE* f, const std::function& modifier) { - WI_VERIFY(f != nullptr); - - auto data = ReadJsonFile(f); + auto data = ReadCredentialFile(f); if (modifier(data)) { - WriteJsonFile(f, data); + WriteCredentialFile(f, data); } } @@ -180,8 +183,8 @@ void FileCredStorage::Store(const std::string& serverAddress, const std::string& { auto file = RetryOpenFileOnSharingViolation(CreateFileExclusive); - ModifyFileStore(file.get(), [&](nlohmann::json& data) { - data[serverAddress] = Protect(credential); + ModifyFileStore(file.get(), [&](CredentialFile& data) { + data.Credentials[serverAddress] = CredentialEntry{Protect(credential)}; return true; }); } @@ -189,47 +192,53 @@ void FileCredStorage::Store(const std::string& serverAddress, const std::string& std::optional FileCredStorage::Get(const std::string& serverAddress) { auto file = RetryOpenFileOnSharingViolation(OpenFileShared); - auto data = ReadJsonFile(file.get()); + if (!file) + { + return std::nullopt; + } - const auto entry = data.find(serverAddress); - if (entry == data.end() || !entry->is_string()) + auto data = ReadCredentialFile(file.get()); + const auto entry = data.Credentials.find(serverAddress); + + if (entry == data.Credentials.end()) { return std::nullopt; } - return Unprotect(entry->get()); + return Unprotect(entry->second.RegistryAuthenticationInformation); } void FileCredStorage::Erase(const std::string& serverAddress) { auto file = RetryOpenFileOnSharingViolation(OpenFileExclusive); - if (!file) + bool erased = false; + + if (file) { - THROW_HR_WITH_USER_ERROR(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress))); + ModifyFileStore(file.get(), [&](CredentialFile& data) { + erased = data.Credentials.erase(serverAddress) > 0; + return erased; + }); } - bool erased = false; - ModifyFileStore(file.get(), [&](nlohmann::json& data) { - erased = data.erase(serverAddress) > 0; - return erased; - }); - THROW_HR_WITH_USER_ERROR_IF(E_NOT_SET, Localization::WSLCCLI_LogoutNotFound(wsl::shared::string::MultiByteToWide(serverAddress)), !erased); } std::vector FileCredStorage::List() { auto file = RetryOpenFileOnSharingViolation(OpenFileShared); - auto data = ReadJsonFile(file.get()); + if (!file) + { + return {}; + } + + auto data = ReadCredentialFile(file.get()); std::vector result; - for (const auto& [key, value] : data.items()) + for (const auto& [key, value] : data.Credentials) { - if (value.is_string()) - { - result.push_back(wsl::shared::string::MultiByteToWide(key)); - } + result.push_back(wsl::shared::string::MultiByteToWide(key)); } return result; diff --git a/src/windows/wslc/services/FileCredStorage.h b/src/windows/wslc/services/FileCredStorage.h index 03bf511a9..5b13c3c1f 100644 --- a/src/windows/wslc/services/FileCredStorage.h +++ b/src/windows/wslc/services/FileCredStorage.h @@ -14,9 +14,26 @@ Module Name: #pragma once #include "ICredentialStorage.h" +#include "JsonUtils.h" namespace wsl::windows::wslc::services { +inline constexpr int CredentialFileVersion = 1; + +struct CredentialEntry +{ + std::string RegistryAuthenticationInformation; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialEntry, RegistryAuthenticationInformation); +}; + +struct CredentialFile +{ + int Version = CredentialFileVersion; + std::map Credentials; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialFile, Version, Credentials); +}; + class FileCredStorage final : public ICredentialStorage { public: diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index 5403d7a2f..6dcb9b6cc 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -144,6 +144,41 @@ class WSLCE2ERegistryTests VERIFY_IS_TRUE(result.Stderr->find(L"Must provide --username with --password-stdin") != std::wstring::npos); } + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_InvalidCredentials) + { + auto session = OpenDefaultElevatedSession(); + + { + auto [registryContainer, registryAddress] = StartLocalRegistry(*session, c_username, c_password, 15003); + auto registryAddressW = string::MultiByteToWide(registryAddress); + + // Login with wrong password should fail. + { + auto result = RunWslc(std::format(L"login -u {} -p wrongpassword {}", string::MultiByteToWide(c_username), registryAddressW)); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"401 Unauthorized") != std::wstring::npos); + } + + // Login with wrong username should fail. + { + auto result = RunWslc(std::format(L"login -u wronguser -p {} {}", string::MultiByteToWide(c_password), registryAddressW)); + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); + VERIFY_IS_TRUE(result.Stderr.has_value()); + VERIFY_IS_TRUE(result.Stderr->find(L"401 Unauthorized") != std::wstring::npos); + } + + // Login with correct credentials should still succeed after failed attempts. + { + auto result = RunWslc(std::format( + L"login -u {} -p {} {}", string::MultiByteToWide(c_username), string::MultiByteToWide(c_password), registryAddressW)); + result.Verify({.Stdout = Localization::WSLCCLI_LoginSucceeded() + L"\r\n", .Stderr = L"", .ExitCode = 0}); + + VerifyLogoutSucceeds(registryAddressW); + } + } + } + WSLC_TEST_METHOD(WSLCE2E_Registry_Login_CredentialInputMethods) { auto session = OpenDefaultElevatedSession(); From 0306a7b1310d2c037611805cf7e6c148b0c132b9 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 15:27:19 -0700 Subject: [PATCH 67/76] fix formatting --- test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index 6dcb9b6cc..1ba55c545 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -154,7 +154,8 @@ class WSLCE2ERegistryTests // Login with wrong password should fail. { - auto result = RunWslc(std::format(L"login -u {} -p wrongpassword {}", string::MultiByteToWide(c_username), registryAddressW)); + auto result = + RunWslc(std::format(L"login -u {} -p wrongpassword {}", string::MultiByteToWide(c_username), registryAddressW)); VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0)); VERIFY_IS_TRUE(result.Stderr.has_value()); VERIFY_IS_TRUE(result.Stderr->find(L"401 Unauthorized") != std::wstring::npos); From 41eece65ef0d27b0b303b644db38327000cefcc5 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 15:44:07 -0700 Subject: [PATCH 68/76] fix formatting --- src/windows/wslc/services/FileCredStorage.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index a6e486d7a..d151f232c 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -134,12 +134,13 @@ void WriteCredentialFile(FILE* f, const CredentialFile& data) auto content = nlohmann::json(data).dump(2); auto written = fwrite(content.data(), 1, content.size(), f); - THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), written != content.size()); + THROW_HR_WITH_USER_ERROR_IF( + E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), written != content.size()); } void ModifyFileStore(FILE* f, const std::function& modifier) { - auto data = ReadCredentialFile(f); + auto data = ReadCredentialFile(f); if (modifier(data)) { @@ -199,7 +200,7 @@ std::optional FileCredStorage::Get(const std::string& serverAddress auto data = ReadCredentialFile(file.get()); const auto entry = data.Credentials.find(serverAddress); - + if (entry == data.Credentials.end()) { return std::nullopt; From ee70f216c4d6823264886ad4d4a04c3d2053dd1b Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 16:46:36 -0700 Subject: [PATCH 69/76] Fix com mta gap issue --- test/windows/Common.cpp | 6 ++++++ test/windows/WSLCTests.cpp | 1 - test/windows/WslcSdkTests.cpp | 1 - test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 2 -- test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 2 -- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index 7d526f54c..c1f70d4b4 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -69,6 +69,7 @@ std::wstring g_testDistroPath; std::wstring g_testDataPath; bool g_fastTestRun = false; // True when test.bat was invoked with -f std::optional> g_dumpKeyChange; +static wil::unique_mta_usage_cookie g_mtaCookie; std::pair CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa) { @@ -1975,6 +1976,11 @@ Return Value: { wsl::windows::common::wslutil::InitializeWil(); + // Keep the MTA alive for the entire test module lifetime. Without this, + // the per-class MTA cookies leave a gap during class transitions where COM + // can unload WinRT DLLs, leaving stale pointers in the C++/WinRT factory cache. + THROW_IF_FAILED(CoIncrementMTAUsage(&g_mtaCookie)); + // Don't crash for unknown exceptions (makes debugging testpasses harder) #ifndef _DEBUG wil::g_fResultFailFastUnknownExceptions = false; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 15ec47440..abced2005 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -36,7 +36,6 @@ class WSLCTests { WSLC_TEST_CLASS(WSLCTests) - wil::unique_mta_usage_cookie m_mtaCookie; WSADATA m_wsadata; std::filesystem::path m_storagePath; WSLCSessionSettings m_defaultSessionSettings{}; diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index cc23d5c0a..96e1e88dc 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -168,7 +168,6 @@ class WslcSdkTests { WSLC_TEST_CLASS(WslcSdkTests) - wil::unique_mta_usage_cookie m_mtaCookie; WSADATA m_wsadata; std::filesystem::path m_storagePath; WslcSession m_defaultSession = nullptr; diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index 0bd4b0d19..76dc0eae8 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -25,8 +25,6 @@ class WSLCE2EPushPullTests { WSLC_TEST_CLASS(WSLCE2EPushPullTests) - wil::unique_mta_usage_cookie m_mtaCookie; - TEST_CLASS_SETUP(TestClassSetup) { THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index 1ba55c545..5615c3462 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -46,8 +46,6 @@ class WSLCE2ERegistryTests { WSLC_TEST_CLASS(WSLCE2ERegistryTests) - wil::unique_mta_usage_cookie m_mtaCookie; - TEST_CLASS_SETUP(TestClassSetup) { THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); From 7ba17a9c2e3b50087fa4d776cd2b45007d3942b9 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 17:03:13 -0700 Subject: [PATCH 70/76] Fix bad build --- test/windows/WSLCTests.cpp | 1 - test/windows/WslcSdkTests.cpp | 1 - test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp | 6 ------ test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp | 6 ------ 4 files changed, 14 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index abced2005..ab2d98cee 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -58,7 +58,6 @@ class WSLCTests TEST_CLASS_SETUP(TestClassSetup) { - THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata)); // The WSLC SDK tests use this same storage to reduce pull overhead. diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index 96e1e88dc..ff6d9bff9 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -181,7 +181,6 @@ class WslcSdkTests TEST_CLASS_SETUP(TestClassSetup) { - THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); THROW_IF_WIN32_ERROR(WSAStartup(MAKEWORD(2, 2), &m_wsadata)); // Use the same storage path as WSLC runtime tests to reduce pull overhead. diff --git a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp index 76dc0eae8..aeada2267 100644 --- a/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2EPushPullTests.cpp @@ -25,12 +25,6 @@ class WSLCE2EPushPullTests { WSLC_TEST_CLASS(WSLCE2EPushPullTests) - TEST_CLASS_SETUP(TestClassSetup) - { - THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); - return true; - } - WSLC_TEST_METHOD(WSLCE2E_Image_Push_HelpCommand) { auto result = RunWslc(L"image push --help"); diff --git a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp index 5615c3462..d111f8ef3 100644 --- a/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp +++ b/test/windows/wslc/e2e/WSLCE2ERegistryTests.cpp @@ -46,12 +46,6 @@ class WSLCE2ERegistryTests { WSLC_TEST_CLASS(WSLCE2ERegistryTests) - TEST_CLASS_SETUP(TestClassSetup) - { - THROW_IF_FAILED(CoIncrementMTAUsage(&m_mtaCookie)); - return true; - } - WSLC_TEST_METHOD(WSLCE2E_Registry_LoginLogout_PushPull_AuthFlow) { const auto& debianImage = DebianTestImage(); From 2c9ec26f689334974c4674f4c9a90b0b590d539b Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 18:40:00 -0700 Subject: [PATCH 71/76] Refactor to match docker password length limits --- src/windows/common/wslutil.cpp | 10 ++--- src/windows/common/wslutil.h | 4 +- src/windows/wslc/services/FileCredStorage.cpp | 12 ++--- src/windows/wslc/services/FileCredStorage.h | 9 ++-- .../wslc/services/ICredentialStorage.h | 5 +-- src/windows/wslc/services/ImageService.cpp | 10 ++--- src/windows/wslc/services/RegistryService.cpp | 28 +++++++----- src/windows/wslc/services/RegistryService.h | 9 ++-- src/windows/wslc/services/WinCredStorage.cpp | 24 +++++++--- src/windows/wslc/services/WinCredStorage.h | 4 +- src/windows/wslc/tasks/RegistryTasks.cpp | 4 +- test/windows/Common.cpp | 1 + test/windows/WSLCTests.cpp | 8 ++-- test/windows/WslcSdkTests.cpp | 8 ++-- .../wslc/WSLCCLICredStorageUnitTests.cpp | 45 ++++++++++--------- 15 files changed, 96 insertions(+), 85 deletions(-) diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 59160c00d..38cc4ca6e 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -1511,16 +1511,14 @@ std::string wsl::windows::common::wslutil::Base64Decode(const std::string& encod return result; } -std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress) +std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& username, const std::string& password) { - nlohmann::json authJson = {{"username", username}, {"password", password}, {"serveraddress", serverAddress}}; - + nlohmann::json authJson = {{"username", username}, {"password", password}}; return Base64Encode(authJson.dump()); } -std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress) +std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::string& identityToken) { - nlohmann::json authJson = {{"identitytoken", identityToken}, {"serveraddress", serverAddress}}; - + nlohmann::json authJson = {{"identitytoken", identityToken}}; return Base64Encode(authJson.dump()); } \ No newline at end of file diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index 6d77b310c..2c3f1ef26 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -338,10 +338,10 @@ std::string Base64Decode(const std::string& encoded); // Builds the base64-encoded X-Registry-Auth header value used by Docker APIs // (PullImage, PushImage, etc.) from the given credentials. -std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password, const std::string& serverAddress); +std::string BuildRegistryAuthHeader(const std::string& username, const std::string& password); // Builds the base64-encoded X-Registry-Auth header value from an identity token // returned by Authenticate(). -std::string BuildRegistryAuthHeader(const std::string& identityToken, const std::string& serverAddress); +std::string BuildRegistryAuthHeader(const std::string& identityToken); } // namespace wsl::windows::common::wslutil diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index d151f232c..5fdc4c8d1 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -180,22 +180,22 @@ std::string Unprotect(const std::string& cipherBase64) namespace wsl::windows::wslc::services { -void FileCredStorage::Store(const std::string& serverAddress, const std::string& credential) +void FileCredStorage::Store(const std::string& serverAddress, const std::string& username, const std::string& secret) { auto file = RetryOpenFileOnSharingViolation(CreateFileExclusive); ModifyFileStore(file.get(), [&](CredentialFile& data) { - data.Credentials[serverAddress] = CredentialEntry{Protect(credential)}; + data.Credentials[serverAddress] = CredentialEntry{username, Protect(secret)}; return true; }); } -std::optional FileCredStorage::Get(const std::string& serverAddress) +std::pair FileCredStorage::Get(const std::string& serverAddress) { auto file = RetryOpenFileOnSharingViolation(OpenFileShared); if (!file) { - return std::nullopt; + return {}; } auto data = ReadCredentialFile(file.get()); @@ -203,10 +203,10 @@ std::optional FileCredStorage::Get(const std::string& serverAddress if (entry == data.Credentials.end()) { - return std::nullopt; + return {}; } - return Unprotect(entry->second.RegistryAuthenticationInformation); + return {entry->second.UserName, Unprotect(entry->second.Secret)}; } void FileCredStorage::Erase(const std::string& serverAddress) diff --git a/src/windows/wslc/services/FileCredStorage.h b/src/windows/wslc/services/FileCredStorage.h index 5b13c3c1f..5b03e7c84 100644 --- a/src/windows/wslc/services/FileCredStorage.h +++ b/src/windows/wslc/services/FileCredStorage.h @@ -22,8 +22,9 @@ inline constexpr int CredentialFileVersion = 1; struct CredentialEntry { - std::string RegistryAuthenticationInformation; - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialEntry, RegistryAuthenticationInformation); + std::string UserName; + std::string Secret; + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CredentialEntry, UserName, Secret); }; struct CredentialFile @@ -37,8 +38,8 @@ struct CredentialFile class FileCredStorage final : public ICredentialStorage { public: - void Store(const std::string& serverAddress, const std::string& credential) override; - std::optional Get(const std::string& serverAddress) override; + void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) override; + std::pair Get(const std::string& serverAddress) override; void Erase(const std::string& serverAddress) override; std::vector List() override; }; diff --git a/src/windows/wslc/services/ICredentialStorage.h b/src/windows/wslc/services/ICredentialStorage.h index 85114a4f9..0ceef96a2 100644 --- a/src/windows/wslc/services/ICredentialStorage.h +++ b/src/windows/wslc/services/ICredentialStorage.h @@ -14,7 +14,6 @@ Module Name: #pragma once #include -#include #include #include @@ -25,8 +24,8 @@ struct ICredentialStorage { virtual ~ICredentialStorage() = default; - virtual void Store(const std::string& serverAddress, const std::string& credential) = 0; - virtual std::optional Get(const std::string& serverAddress) = 0; + virtual void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) = 0; + virtual std::pair Get(const std::string& serverAddress) = 0; virtual void Erase(const std::string& serverAddress) = 0; virtual std::vector List() = 0; }; diff --git a/src/windows/wslc/services/ImageService.cpp b/src/windows/wslc/services/ImageService.cpp index 3f6b5de5d..0910c4f0a 100644 --- a/src/windows/wslc/services/ImageService.cpp +++ b/src/windows/wslc/services/ImageService.cpp @@ -205,10 +205,8 @@ void ImageService::Delete(wsl::windows::wslc::models::Session& session, const st void ImageService::Pull(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback) { auto server = GetServerFromImage(image); - auto storedAuth = RegistryService::Get(server); - auto auth = storedAuth.has_value() ? storedAuth->c_str() : nullptr; - - THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), auth, callback)); + auto auth = RegistryService::Get(server); + THROW_IF_FAILED(session.Get()->PullImage(image.c_str(), auth.c_str(), callback)); } void ImageService::Tag(wsl::windows::wslc::models::Session& session, const std::string& sourceImage, const std::string& targetImage) @@ -238,9 +236,7 @@ InspectImage ImageService::Inspect(wsl::windows::wslc::models::Session& session, void ImageService::Push(wsl::windows::wslc::models::Session& session, const std::string& image, IProgressCallback* callback) { auto server = GetServerFromImage(image); - auto storedAuth = RegistryService::Get(server); - auto auth = storedAuth.value_or(BuildRegistryAuthHeader("", "", server)); - + auto auth = RegistryService::Get(server); THROW_IF_FAILED(session.Get()->PushImage(image.c_str(), auth.c_str(), callback)); } diff --git a/src/windows/wslc/services/RegistryService.cpp b/src/windows/wslc/services/RegistryService.cpp index db8aab1c6..5e227d29b 100644 --- a/src/windows/wslc/services/RegistryService.cpp +++ b/src/windows/wslc/services/RegistryService.cpp @@ -47,24 +47,30 @@ std::string ResolveCredentialKey(const std::string& serverAddress) namespace wsl::windows::wslc::services { -void RegistryService::Store(const std::string& serverAddress, const std::string& credential) +// Sentinel username matching Docker's convention for identity-token credentials. +static constexpr auto TokenUsername = ""; + +void RegistryService::Store(const std::string& serverAddress, const std::string& username, const std::string& secret) { THROW_HR_IF(E_INVALIDARG, serverAddress.empty()); - THROW_HR_IF(E_INVALIDARG, credential.empty()); + THROW_HR_IF(E_INVALIDARG, secret.empty()); auto storage = OpenCredentialStorage(); - storage->Store(ResolveCredentialKey(serverAddress), credential); + storage->Store(ResolveCredentialKey(serverAddress), username, secret); } -std::optional RegistryService::Get(const std::string& serverAddress) +std::string RegistryService::Get(const std::string& serverAddress) { - if (serverAddress.empty()) + auto storage = OpenCredentialStorage(); + auto key = ResolveCredentialKey(serverAddress); + auto [username, secret] = storage->Get(key); + + if (username == TokenUsername) { - return std::nullopt; + return BuildRegistryAuthHeader(secret); } - auto storage = OpenCredentialStorage(); - return storage->Get(ResolveCredentialKey(serverAddress)); + return BuildRegistryAuthHeader(username, secret); } void RegistryService::Erase(const std::string& serverAddress) @@ -81,7 +87,7 @@ std::vector RegistryService::List() return storage->List(); } -std::string RegistryService::Authenticate( +std::pair RegistryService::Authenticate( wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password) { wil::unique_cotaskmem_ansistring identityToken; @@ -90,10 +96,10 @@ std::string RegistryService::Authenticate( // If the registry returned an identity token, use it. Otherwise fall back to username/password. if (identityToken && strlen(identityToken.get()) > 0) { - return BuildRegistryAuthHeader(identityToken.get(), serverAddress); + return {TokenUsername, identityToken.get()}; } - return BuildRegistryAuthHeader(username, password, serverAddress); + return {username, password}; } } // namespace wsl::windows::wslc::services diff --git a/src/windows/wslc/services/RegistryService.h b/src/windows/wslc/services/RegistryService.h index 6f7b72b4b..5494c29ec 100644 --- a/src/windows/wslc/services/RegistryService.h +++ b/src/windows/wslc/services/RegistryService.h @@ -23,14 +23,11 @@ namespace wsl::windows::wslc::services { class RegistryService { public: - static void Store(const std::string& serverAddress, const std::string& credential); - static std::optional Get(const std::string& serverAddress); + static void Store(const std::string& serverAddress, const std::string& username, const std::string& secret); + static std::string Get(const std::string& serverAddress); static void Erase(const std::string& serverAddress); static std::vector List(); - - // Authenticates with a registry via the session's Docker engine. - // Returns a base64-encoded auth header ready to store and pass to push/pull. - static std::string Authenticate( + static std::pair Authenticate( wsl::windows::wslc::models::Session& session, const std::string& serverAddress, const std::string& username, const std::string& password); // Default registry server address used when no explicit server is provided. diff --git a/src/windows/wslc/services/WinCredStorage.cpp b/src/windows/wslc/services/WinCredStorage.cpp index 13c4a40d5..03cfd9de6 100644 --- a/src/windows/wslc/services/WinCredStorage.cpp +++ b/src/windows/wslc/services/WinCredStorage.cpp @@ -30,21 +30,23 @@ std::wstring WinCredStorage::TargetName(const std::string& serverAddress) return std::wstring(WinCredPrefix) + wsl::shared::string::MultiByteToWide(serverAddress); } -void WinCredStorage::Store(const std::string& serverAddress, const std::string& credential) +void WinCredStorage::Store(const std::string& serverAddress, const std::string& username, const std::string& secret) { auto targetName = TargetName(serverAddress); + auto wideUsername = wsl::shared::string::MultiByteToWide(username); CREDENTIALW cred{}; cred.Type = CRED_TYPE_GENERIC; cred.TargetName = const_cast(targetName.c_str()); - cred.CredentialBlobSize = static_cast(credential.size()); - cred.CredentialBlob = reinterpret_cast(const_cast(credential.data())); + cred.UserName = const_cast(wideUsername.c_str()); + cred.CredentialBlobSize = static_cast(secret.size()); + cred.CredentialBlob = reinterpret_cast(const_cast(secret.data())); cred.Persist = CRED_PERSIST_LOCAL_MACHINE; THROW_IF_WIN32_BOOL_FALSE(CredWriteW(&cred, 0)); } -std::optional WinCredStorage::Get(const std::string& serverAddress) +std::pair WinCredStorage::Get(const std::string& serverAddress) { auto targetName = TargetName(serverAddress); @@ -52,15 +54,23 @@ std::optional WinCredStorage::Get(const std::string& serverAddress) if (!CredReadW(targetName.c_str(), CRED_TYPE_GENERIC, 0, &cred)) { THROW_LAST_ERROR_IF(GetLastError() != ERROR_NOT_FOUND); - return std::nullopt; + return {}; } if (cred.get()->CredentialBlobSize == 0 || cred.get()->CredentialBlob == nullptr) { - return std::nullopt; + return {}; + } + + std::string username; + if (cred.get()->UserName) + { + username = wsl::shared::string::WideToMultiByte(cred.get()->UserName); } - return std::string(reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize); + return { + std::move(username), + {reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize}}; } void WinCredStorage::Erase(const std::string& serverAddress) diff --git a/src/windows/wslc/services/WinCredStorage.h b/src/windows/wslc/services/WinCredStorage.h index fe4a68099..35dd2845e 100644 --- a/src/windows/wslc/services/WinCredStorage.h +++ b/src/windows/wslc/services/WinCredStorage.h @@ -20,8 +20,8 @@ namespace wsl::windows::wslc::services { class WinCredStorage final : public ICredentialStorage { public: - void Store(const std::string& serverAddress, const std::string& credential) override; - std::optional Get(const std::string& serverAddress) override; + void Store(const std::string& serverAddress, const std::string& username, const std::string& secret) override; + std::pair Get(const std::string& serverAddress) override; void Erase(const std::string& serverAddress) override; std::vector List() override; diff --git a/src/windows/wslc/tasks/RegistryTasks.cpp b/src/windows/wslc/tasks/RegistryTasks.cpp index 078122a71..279a1ead4 100644 --- a/src/windows/wslc/tasks/RegistryTasks.cpp +++ b/src/windows/wslc/tasks/RegistryTasks.cpp @@ -43,8 +43,8 @@ void Login(CLIExecutionContext& context) serverAddress = WideToMultiByte(context.Args.Get()); } - auto auth = RegistryService::Authenticate(session, serverAddress, username, password); - RegistryService::Store(serverAddress, auth); + auto [credUsername, credSecret] = RegistryService::Authenticate(session, serverAddress, username, password); + RegistryService::Store(serverAddress, credUsername, credSecret); PrintMessage(Localization::WSLCCLI_LoginSucceeded()); } diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index c1f70d4b4..0c2f6064b 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -2209,6 +2209,7 @@ Return Value: } WslTraceLoggingUninitialize(); + g_mtaCookie.reset(); return true; } diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index ab2d98cee..91fcd6162 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -480,7 +480,7 @@ class WSLCTests // Start a local registry without auth and push hello-world:latest to it. auto [registryContainer, registryAddress] = StartLocalRegistry(); - auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", "", registryAddress)); + auto image = PushImageToRegistry("hello-world:latest", registryAddress, BuildRegistryAuthHeader("", "")); ExpectImagePresent(*m_defaultSession, image.c_str(), false); VERIFY_SUCCEEDED(m_defaultSession->PullImage(image.c_str(), nullptr, nullptr)); @@ -522,7 +522,7 @@ class WSLCTests { // Start a local registry without auth to avoid Docker Hub rate limits. auto [registryContainer, registryAddress] = StartLocalRegistry(); - auto auth = BuildRegistryAuthHeader("", "", registryAddress); + auto auth = BuildRegistryAuthHeader("", ""); auto validatePull = [&](const std::string& sourceImage) { // Push the source image to the local registry. @@ -604,7 +604,7 @@ class WSLCTests WSLC_TEST_METHOD(PushImage) { - auto emptyAuth = BuildRegistryAuthHeader("", "", ""); + auto emptyAuth = BuildRegistryAuthHeader("", ""); // Validate that pushing a non-existent image fails. { @@ -640,7 +640,7 @@ class WSLCTests VERIFY_SUCCEEDED(m_defaultSession->Authenticate(registryAddress.c_str(), c_username, c_password, &token)); VERIFY_IS_NOT_NULL(token.get()); - auto xRegistryAuth = BuildRegistryAuthHeader(c_username, c_password, registryAddress); + auto xRegistryAuth = BuildRegistryAuthHeader(c_username, c_password); auto image = PushImageToRegistry("hello-world:latest", registryAddress, xRegistryAuth); // Pulling without credentials should fail. diff --git a/test/windows/WslcSdkTests.cpp b/test/windows/WslcSdkTests.cpp index ff6d9bff9..8560d0ac3 100644 --- a/test/windows/WslcSdkTests.cpp +++ b/test/windows/WslcSdkTests.cpp @@ -2111,7 +2111,7 @@ class WslcSdkTests VERIFY_IS_NOT_NULL(token.get()); } - auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password, registryAddress); + auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, c_password); PushImageToRegistry("hello-world", "latest", registryAddress, xRegistryAuth); auto image = std::format("{}/hello-world:latest", registryAddress); @@ -2137,7 +2137,7 @@ class WslcSdkTests // Negative: Pulling with bad credentials should fail. { - auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong", registryAddress); + auto badAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader(c_username, "wrong"); WslcPullImageOptions opts{}; opts.uri = image.c_str(); @@ -2162,7 +2162,7 @@ class WslcSdkTests { // Start a local registry without auth to avoid Docker Hub rate limits. auto [registryContainer, registryAddress] = StartLocalRegistry(); - auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "", registryAddress); + auto xRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", ""); { // Push hello-world:latest to the local registry. @@ -2212,7 +2212,7 @@ class WslcSdkTests WSLC_TEST_METHOD(PushImage) { - auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", "", ""); + auto emptyRegistryAuth = wsl::windows::common::wslutil::BuildRegistryAuthHeader("", ""); // Negative: pushing a non-existent image must fail. { diff --git a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp index 7b890170f..1f7a9a1ab 100644 --- a/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp +++ b/test/windows/wslc/WSLCCLICredStorageUnitTests.cpp @@ -35,11 +35,11 @@ class WSLCCLICredStorageUnitTests static void TestStoreAndGetRoundTrips(ICredentialStorage& storage) { auto cleanup = wil::scope_exit([&]() { storage.Erase("wslc-test-server1"); }); - storage.Store("wslc-test-server1", "credential-data-1"); + storage.Store("wslc-test-server1", "test-user", "credential-data-1"); - auto result = storage.Get("wslc-test-server1"); - VERIFY_IS_TRUE(result.has_value()); - VERIFY_ARE_EQUAL(std::string("credential-data-1"), result.value()); + auto [username, secret] = storage.Get("wslc-test-server1"); + VERIFY_ARE_EQUAL(std::string("test-user"), username); + VERIFY_ARE_EQUAL(std::string("credential-data-1"), secret); } TEST_METHOD(FileCred_Store_And_Get_RoundTrips) @@ -51,30 +51,31 @@ class WSLCCLICredStorageUnitTests TestStoreAndGetRoundTrips(m_winCredStorage); } - static void TestGetNonExistentReturnsNullopt(ICredentialStorage& storage) + static void TestGetNonExistentReturnsEmpty(ICredentialStorage& storage) { - auto result = storage.Get("wslc-test-nonexistent-server"); - VERIFY_IS_FALSE(result.has_value()); + auto [username, secret] = storage.Get("wslc-test-nonexistent-server"); + VERIFY_IS_TRUE(username.empty()); + VERIFY_IS_TRUE(secret.empty()); } - TEST_METHOD(FileCred_Get_NonExistent_ReturnsNullopt) + TEST_METHOD(FileCred_Get_NonExistent_ReturnsEmpty) { - TestGetNonExistentReturnsNullopt(m_fileStorage); + TestGetNonExistentReturnsEmpty(m_fileStorage); } - TEST_METHOD(WinCred_Get_NonExistent_ReturnsNullopt) + TEST_METHOD(WinCred_Get_NonExistent_ReturnsEmpty) { - TestGetNonExistentReturnsNullopt(m_winCredStorage); + TestGetNonExistentReturnsEmpty(m_winCredStorage); } static void TestStoreOverwritesExistingCredential(ICredentialStorage& storage) { auto cleanup = wil::scope_exit([&]() { storage.Erase("wslc-test-server2"); }); - storage.Store("wslc-test-server2", "old-credential"); - storage.Store("wslc-test-server2", "new-credential"); + storage.Store("wslc-test-server2", "old-user", "old-credential"); + storage.Store("wslc-test-server2", "new-user", "new-credential"); - auto result = storage.Get("wslc-test-server2"); - VERIFY_IS_TRUE(result.has_value()); - VERIFY_ARE_EQUAL(std::string("new-credential"), result.value()); + auto [username, secret] = storage.Get("wslc-test-server2"); + VERIFY_ARE_EQUAL(std::string("new-user"), username); + VERIFY_ARE_EQUAL(std::string("new-credential"), secret); } TEST_METHOD(FileCred_Store_Overwrites_ExistingCredential) @@ -92,8 +93,8 @@ class WSLCCLICredStorageUnitTests storage.Erase("wslc-test-list1"); storage.Erase("wslc-test-list2"); }); - storage.Store("wslc-test-list1", "cred1"); - storage.Store("wslc-test-list2", "cred2"); + storage.Store("wslc-test-list1", "user1", "cred1"); + storage.Store("wslc-test-list2", "user2", "cred2"); auto servers = storage.List(); bool found1 = false, found2 = false; @@ -124,11 +125,13 @@ class WSLCCLICredStorageUnitTests static void TestEraseRemovesCredential(ICredentialStorage& storage) { - storage.Store("wslc-test-erase", "cred"); - VERIFY_IS_TRUE(storage.Get("wslc-test-erase").has_value()); + storage.Store("wslc-test-erase", "user", "cred"); + auto [username, secret] = storage.Get("wslc-test-erase"); + VERIFY_IS_FALSE(username.empty()); storage.Erase("wslc-test-erase"); - VERIFY_IS_FALSE(storage.Get("wslc-test-erase").has_value()); + auto [username2, secret2] = storage.Get("wslc-test-erase"); + VERIFY_IS_TRUE(username2.empty()); } TEST_METHOD(FileCred_Erase_RemovesCredential) From 5d2db25554da9f69be7baa747e59c7ad8540bc0d Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 18:41:49 -0700 Subject: [PATCH 72/76] Revert "Catch excpetions in test host process" This reverts commit 53c7f635f880d00aaf0dfb2fde963ee771564e70. --- src/windows/common/wslutil.cpp | 2 +- test/windows/Common.cpp | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 38cc4ca6e..21de0f002 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -361,7 +361,7 @@ void wsl::windows::common::wslutil::CoInitializeSecurity() void wsl::windows::common::wslutil::ConfigureCrashHandler() { - SetUnhandledExceptionFilter(OnException); + AddVectoredExceptionHandler(1, OnException); } void wsl::windows::common::wslutil::ConfigureCrt() diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index 0c2f6064b..205ed59d1 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -2060,9 +2060,6 @@ Return Value: g_dumpKeyChange.emplace( HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::wslutil::c_crashFolderKeyName, g_dumpFolder.c_str()); - - // Also install the vectored exception handler in the test host process itself. - wsl::windows::common::wslutil::ConfigureCrashHandler(); } g_WatchdogTimer = CreateThreadpoolTimer(LxsstuWatchdogTimer, nullptr, nullptr); From 98c12c36fa20aa3eb72e4b6d72cde4e355276113 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 18:41:49 -0700 Subject: [PATCH 73/76] Revert "Set Crash dump handler in wslc cloud test" This reverts commit f94022efe177d410d39b41b5f61debdef8445c10. --- cloudtest/TestGroup-wslc.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudtest/TestGroup-wslc.xml.in b/cloudtest/TestGroup-wslc.xml.in index c8f05abbd..888ff604c 100644 --- a/cloudtest/TestGroup-wslc.xml.in +++ b/cloudtest/TestGroup-wslc.xml.in @@ -22,6 +22,6 @@ - + From 3c41068829d0c321ad6aad4e358717595a3b4c6c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 18:43:26 -0700 Subject: [PATCH 74/76] Revert "TEMP: Cherry-pick PR #14244 crash dump handler (to be reverted)" This reverts commit 3a2f454633dd9ab6b93cf4167cc30d1941bca267. --- CMakeLists.txt | 1 - cloudtest/TestGroup.xml.in | 2 +- src/windows/common/wslutil.cpp | 63 -------------------- src/windows/common/wslutil.h | 3 - src/windows/service/exe/ServiceMain.cpp | 2 - src/windows/wslc/core/Main.cpp | 2 - src/windows/wslcsession/main.cpp | 2 - src/windows/wslhost/main.cpp | 2 - src/windows/wslinstaller/exe/ServiceMain.cpp | 3 - src/windows/wslrelay/main.cpp | 2 - test/windows/CMakeLists.txt | 1 + test/windows/Common.cpp | 15 ----- 12 files changed, 2 insertions(+), 96 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c0bab27b9..1400001af 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -286,7 +286,6 @@ set(COMMON_LINK_LIBRARIES synchronization.lib Bcrypt.lib Crypt32.lib - Dbghelp.lib icu.lib) set(MSI_LINK_LIBRARIES diff --git a/cloudtest/TestGroup.xml.in b/cloudtest/TestGroup.xml.in index 1caf82381..efa0f59de 100644 --- a/cloudtest/TestGroup.xml.in +++ b/cloudtest/TestGroup.xml.in @@ -22,6 +22,6 @@ - + diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 21de0f002..94634303c 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -22,7 +22,6 @@ Module Name: #include "ConsoleProgressBar.h" #include "ExecutionContext.h" #include "MsiQuery.h" -#include using winrt::Windows::Foundation::Uri; using winrt::Windows::Management::Deployment::DeploymentOptions; @@ -209,8 +208,6 @@ static const std::map g_contextStrings{ #undef X -DEFINE_ENUM_FLAG_OPERATORS(MINIDUMP_TYPE); - wil::unique_hlocal_string GetWinInetErrorString(HRESULT error) { const wil::unique_hmodule library{LoadLibrary(L"WinInet.dll")}; @@ -264,61 +261,6 @@ constexpr GUID EndianSwap(GUID value) return value; } -static LONG WINAPI OnException(_EXCEPTION_POINTERS* exception) -{ - try - { - static std::atomic handlingException = false; - if (handlingException.exchange(true)) - { - return EXCEPTION_CONTINUE_SEARCH; // Don't keep trying if we crash during exception handling. - } - - // Collect a crash dump if enabled. - auto image = std::filesystem::path(wil::GetModuleFileNameW()).filename(); - - auto lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_READ); - auto crashFolder = wsl::windows::common::registry::ReadOptionalString(lxssKey.get(), nullptr, c_crashFolderKeyName); - - std::optional dumpPath; - if (crashFolder.has_value()) - { - dumpPath = std::filesystem::path(crashFolder.value()) / std::format(L"{}.{}.dmp", image.native(), GetCurrentProcessId()); - } - - WSL_LOG( - "ProcessCrash", - TraceLoggingValue(image.c_str(), "Process"), - TraceLoggingValue(dumpPath.has_value() ? dumpPath->native().c_str() : L"", "DumpPath")); - - if (!dumpPath.has_value()) - { - return EXCEPTION_CONTINUE_SEARCH; - } - - auto dumpFile = wil::create_new_file(dumpPath->c_str(), GENERIC_WRITE, FILE_SHARE_READ); - THROW_LAST_ERROR_IF(!dumpFile); - - MINIDUMP_EXCEPTION_INFORMATION exceptionInfo{}; - exceptionInfo.ThreadId = GetCurrentThreadId(); - exceptionInfo.ExceptionPointers = exception; - - THROW_IF_WIN32_BOOL_FALSE(MiniDumpWriteDump( - GetCurrentProcess(), - GetCurrentProcessId(), - dumpFile.get(), - MiniDumpWithDataSegs | MiniDumpWithFullMemory | MiniDumpWithProcessThreadData | MiniDumpWithHandleData | - MiniDumpWithPrivateReadWriteMemory | MiniDumpWithUnloadedModules | MiniDumpWithFullMemoryInfo | - MiniDumpWithThreadInfo | MiniDumpWithTokenInformation | MiniDumpWithPrivateWriteCopyMemory | MiniDumpWithCodeSegs, - &exceptionInfo, - nullptr, - nullptr)); - } - CATCH_LOG(); - - return EXCEPTION_CONTINUE_SEARCH; -} - std::regex BuildImageReferenceRegex() { // See: https://github.com/containers/image/blob/main/docker/reference/regexp.go @@ -359,11 +301,6 @@ void wsl::windows::common::wslutil::CoInitializeSecurity() nullptr, -1, nullptr, nullptr, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_STATIC_CLOAKING, 0)); } -void wsl::windows::common::wslutil::ConfigureCrashHandler() -{ - AddVectoredExceptionHandler(1, OnException); -} - void wsl::windows::common::wslutil::ConfigureCrt() { // _CALL_REPORTFAULT will cause the process to actually crash instead of just exiting. diff --git a/src/windows/common/wslutil.h b/src/windows/common/wslutil.h index 2c3f1ef26..873771723 100644 --- a/src/windows/common/wslutil.h +++ b/src/windows/common/wslutil.h @@ -42,7 +42,6 @@ inline constexpr GUID WslTerminalNamespace = {0xbe9372fe, 0x59e1, 0x4876, {0xbd, // {2bde4a90-d05f-401c-9492-e40884ead1d8} inline constexpr GUID GeneratedProfilesTerminalNamespace = {0x2bde4a90, 0xd05f, 0x401c, {0x94, 0x92, 0xe4, 0x8, 0x84, 0xea, 0xd1, 0xd8}}; -inline auto c_crashFolderKeyName = L"CrashDumpFolder"; inline auto c_msixPackageFamilyName = L"MicrosoftCorporationII.WindowsSubsystemForLinux_8wekyb3d8bbwe"; inline auto c_githubUrlOverrideRegistryValue = L"GitHubUrlOverride"; inline auto c_vhdFileExtension = L".vhd"; @@ -188,8 +187,6 @@ wil::com_ptr CoGetCallContext(); void CoInitializeSecurity(); -void ConfigureCrashHandler(); - void ConfigureCrt(); /// diff --git a/src/windows/service/exe/ServiceMain.cpp b/src/windows/service/exe/ServiceMain.cpp index 617e85ee5..059f7d599 100644 --- a/src/windows/service/exe/ServiceMain.cpp +++ b/src/windows/service/exe/ServiceMain.cpp @@ -169,8 +169,6 @@ try WSL_LOG("Service starting", TraceLoggingLevel(WINEVENT_LEVEL_INFO)); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Don't kill the process on unknown C++ exceptions. wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslc/core/Main.cpp b/src/windows/wslc/core/Main.cpp index ec9a215f6..8278130b7 100644 --- a/src/windows/wslc/core/Main.cpp +++ b/src/windows/wslc/core/Main.cpp @@ -39,8 +39,6 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanupTelemetry = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - wslutil::SetCrtEncoding(_O_U8TEXT); auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslcsession/main.cpp b/src/windows/wslcsession/main.cpp index 5e70713c2..d04d10c26 100644 --- a/src/windows/wslcsession/main.cpp +++ b/src/windows/wslcsession/main.cpp @@ -62,8 +62,6 @@ try WslTraceLoggingInitialize(WslcTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit([] { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Don't kill the process on unknown C++ exceptions wil::g_fResultFailFastUnknownExceptions = false; diff --git a/src/windows/wslhost/main.cpp b/src/windows/wslhost/main.cpp index 38bf449e7..e89dbe7ab 100644 --- a/src/windows/wslhost/main.cpp +++ b/src/windows/wslhost/main.cpp @@ -167,8 +167,6 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, !wsl::shared::OfficialBuild); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Initialize COM. auto coInit = wil::CoInitializeEx(COINIT_MULTITHREADED); wsl::windows::common::wslutil::CoInitializeSecurity(); diff --git a/src/windows/wslinstaller/exe/ServiceMain.cpp b/src/windows/wslinstaller/exe/ServiceMain.cpp index 4565ee719..dbf835897 100644 --- a/src/windows/wslinstaller/exe/ServiceMain.cpp +++ b/src/windows/wslinstaller/exe/ServiceMain.cpp @@ -71,9 +71,6 @@ HRESULT WslInstallerService::OnServiceStarting() wsl::windows::common::wslutil::ConfigureCrt(); WslTraceLoggingInitialize(WslServiceTelemetryProvider, !wsl::shared::OfficialBuild); - - wsl::windows::common::wslutil::ConfigureCrashHandler(); - wsl::windows::common::security::ApplyProcessMitigationPolicies(); return S_OK; diff --git a/src/windows/wslrelay/main.cpp b/src/windows/wslrelay/main.cpp index 74ca79568..ea5080be2 100644 --- a/src/windows/wslrelay/main.cpp +++ b/src/windows/wslrelay/main.cpp @@ -58,8 +58,6 @@ try WslTraceLoggingInitialize(LxssTelemetryProvider, disableTelemetry); auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { WslTraceLoggingUninitialize(); }); - wsl::windows::common::wslutil::ConfigureCrashHandler(); - // Ensure that the other end of the pipe has connected if required. if (connectPipe) { diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index da559506c..ee55f617c 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -36,6 +36,7 @@ target_link_libraries(wsltests ${SERVICE_LINK_LIBRARIES} VirtDisk.lib Wer.lib + Dbghelp.lib sfc.lib Crypt32.lib) diff --git a/test/windows/Common.cpp b/test/windows/Common.cpp index 205ed59d1..00f3467bf 100644 --- a/test/windows/Common.cpp +++ b/test/windows/Common.cpp @@ -68,7 +68,6 @@ static std::wstring g_pipelineBuildId; std::wstring g_testDistroPath; std::wstring g_testDataPath; bool g_fastTestRun = false; // True when test.bat was invoked with -f -std::optional> g_dumpKeyChange; static wil::unique_mta_usage_cookie g_mtaCookie; std::pair CreateSubprocessPipe(bool inheritRead, bool inheritWrite, DWORD bufferSize, _In_opt_ SECURITY_ATTRIBUTES* sa) @@ -1976,9 +1975,6 @@ Return Value: { wsl::windows::common::wslutil::InitializeWil(); - // Keep the MTA alive for the entire test module lifetime. Without this, - // the per-class MTA cookies leave a gap during class transitions where COM - // can unload WinRT DLLs, leaving stale pointers in the C++/WinRT factory cache. THROW_IF_FAILED(CoIncrementMTAUsage(&g_mtaCookie)); // Don't crash for unknown exceptions (makes debugging testpasses harder) @@ -2051,17 +2047,6 @@ Return Value: WEX::TestExecution::RuntimeParameters::TryGetValue(L"WerReport", g_enableWerReport); WEX::TestExecution::RuntimeParameters::TryGetValue(L"LogDmesg", g_LogDmesgAfterEachTest); - bool enableCrashDumpCollection = false; - WEX::TestExecution::RuntimeParameters::TryGetValue(L"CollectCrashDumps", enableCrashDumpCollection); - - if (enableCrashDumpCollection) - { - LogInfo("Enabling crash dump collection. Target: %ls", g_dumpFolder.c_str()); - - g_dumpKeyChange.emplace( - HKEY_LOCAL_MACHINE, LXSS_REGISTRY_PATH, wsl::windows::common::wslutil::c_crashFolderKeyName, g_dumpFolder.c_str()); - } - g_WatchdogTimer = CreateThreadpoolTimer(LxsstuWatchdogTimer, nullptr, nullptr); VERIFY_IS_NOT_NULL(g_WatchdogTimer); From a4c989b6d43bab728371d3c34530f482388c55d0 Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 18:52:35 -0700 Subject: [PATCH 75/76] Fix whitespace --- src/windows/WslcSDK/wslcsdk.h | 2 +- src/windows/common/WSLCContainerLauncher.cpp | 1 + src/windows/common/wslutil.cpp | 2 +- test/windows/WSLCTests.cpp | 2 +- test/windows/wslc/e2e/WSLCE2EHelpers.h | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/windows/WslcSDK/wslcsdk.h b/src/windows/WslcSDK/wslcsdk.h index 3a9dfccc3..5aa51dc84 100644 --- a/src/windows/WslcSDK/wslcsdk.h +++ b/src/windows/WslcSDK/wslcsdk.h @@ -564,4 +564,4 @@ typedef __callback void(CALLBACK* WslcInstallCallback)(_In_ WslcComponentFlags c STDAPI WslcInstallWithDependencies(_In_opt_ WslcInstallCallback progressCallback, _In_opt_ PVOID context); -EXTERN_C_END \ No newline at end of file +EXTERN_C_END diff --git a/src/windows/common/WSLCContainerLauncher.cpp b/src/windows/common/WSLCContainerLauncher.cpp index 53ce52cf3..78eb56403 100644 --- a/src/windows/common/WSLCContainerLauncher.cpp +++ b/src/windows/common/WSLCContainerLauncher.cpp @@ -11,6 +11,7 @@ Module Name: This file contains the implementation for WSLCContainerLauncher. --*/ + #include "precomp.h" #include "WSLCContainerLauncher.h" diff --git a/src/windows/common/wslutil.cpp b/src/windows/common/wslutil.cpp index 94634303c..b947c2603 100644 --- a/src/windows/common/wslutil.cpp +++ b/src/windows/common/wslutil.cpp @@ -1458,4 +1458,4 @@ std::string wsl::windows::common::wslutil::BuildRegistryAuthHeader(const std::st { nlohmann::json authJson = {{"identitytoken", identityToken}}; return Base64Encode(authJson.dump()); -} \ No newline at end of file +} diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 91fcd6162..0ad9d6c0d 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -7120,4 +7120,4 @@ class WSLCTests ValidateProcessOutput(initProcess, {{1, "OK\n"}}); } -}; \ No newline at end of file +}; diff --git a/test/windows/wslc/e2e/WSLCE2EHelpers.h b/test/windows/wslc/e2e/WSLCE2EHelpers.h index 7aadef9f5..1042d07e3 100644 --- a/test/windows/wslc/e2e/WSLCE2EHelpers.h +++ b/test/windows/wslc/e2e/WSLCE2EHelpers.h @@ -192,4 +192,4 @@ std::pair StartLocalReg // Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest"). std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress); -} // namespace WSLCE2ETests \ No newline at end of file +} // namespace WSLCE2ETests From 8331b990da3988a071681b3a34a17ca3bc26b57c Mon Sep 17 00:00:00 2001 From: kvega005 Date: Wed, 15 Apr 2026 19:44:49 -0700 Subject: [PATCH 76/76] Refactor error handling --- localization/strings/en-US/Resources.resw | 4 ++ src/windows/wslc/services/FileCredStorage.cpp | 64 ++++++++++--------- src/windows/wslc/services/WinCredStorage.cpp | 4 +- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index cf61837ea..2f113b4e7 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -2326,6 +2326,10 @@ For privacy information about this product please visit https://aka.ms/privacy.< Failed to parse credentials file '{}': the file may be corrupted. {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + + Failed to write '{}': {} + {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated + Server diff --git a/src/windows/wslc/services/FileCredStorage.cpp b/src/windows/wslc/services/FileCredStorage.cpp index 5fdc4c8d1..56b67336c 100644 --- a/src/windows/wslc/services/FileCredStorage.cpp +++ b/src/windows/wslc/services/FileCredStorage.cpp @@ -47,18 +47,19 @@ wil::unique_file RetryOpenFileOnSharingViolation(const std::function; + + UniqueFd fd; + auto err = _wsopen_s(fd.addressof(), filePath.c_str(), _O_RDWR | _O_CREAT | _O_BINARY, _SH_DENYRW, _S_IREAD | _S_IWRITE); + if (err != 0) + { + auto dosError = _doserrno; + THROW_WIN32_IF(dosError, dosError != 0); + THROW_HR(E_FAIL); + } - wil::unique_file f(_fdopen(fd, "r+b")); + wil::unique_file f(_fdopen(fd.get(), "r+b")); if (!f) { - _close(fd); - THROW_WIN32(_doserrno); + auto dosError = _doserrno; + THROW_WIN32_IF(dosError, dosError != 0); + THROW_HR(E_FAIL); } return f; @@ -84,18 +92,19 @@ wil::unique_file CreateFileExclusive() wil::unique_file OpenFileShared() { wil::unique_file f(_wfsopen(GetFilePath().c_str(), L"rb", _SH_DENYWR)); - if (f) + if (!f) { - return f; - } + auto dosError = _doserrno; + if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND) + { + return nullptr; + } - auto dosError = _doserrno; - if (dosError == ERROR_FILE_NOT_FOUND || dosError == ERROR_PATH_NOT_FOUND) - { - return nullptr; + THROW_WIN32_IF(dosError, dosError != 0); + THROW_HR(E_FAIL); } - THROW_WIN32(dosError); + return f; } CredentialFile ReadCredentialFile(FILE* f) @@ -124,18 +133,15 @@ CredentialFile ReadCredentialFile(FILE* f) void WriteCredentialFile(FILE* f, const CredentialFile& data) { auto error = fseek(f, 0, SEEK_SET); - THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), error != 0); + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), error != 0); error = _chsize_s(_fileno(f), 0); - THROW_HR_WITH_USER_ERROR_IF( - HRESULT_FROM_WIN32(_doserrno), - Localization::MessageWslcFailedToOpenFile(GetFilePath(), GetSystemErrorString(HRESULT_FROM_WIN32(_doserrno))), - error != 0); + THROW_HR_WITH_USER_ERROR_IF(E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(error)), error != 0); auto content = nlohmann::json(data).dump(2); auto written = fwrite(content.data(), 1, content.size(), f); THROW_HR_WITH_USER_ERROR_IF( - E_FAIL, Localization::MessageWslcFailedToOpenFile(GetFilePath(), _wcserror(errno)), written != content.size()); + E_FAIL, Localization::MessageWslcFailedToWriteFile(GetFilePath(), _wcserror(errno)), written != content.size()); } void ModifyFileStore(FILE* f, const std::function& modifier) diff --git a/src/windows/wslc/services/WinCredStorage.cpp b/src/windows/wslc/services/WinCredStorage.cpp index 03cfd9de6..2f4bdf616 100644 --- a/src/windows/wslc/services/WinCredStorage.cpp +++ b/src/windows/wslc/services/WinCredStorage.cpp @@ -68,9 +68,7 @@ std::pair WinCredStorage::Get(const std::string& serve username = wsl::shared::string::WideToMultiByte(cred.get()->UserName); } - return { - std::move(username), - {reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize}}; + return {std::move(username), {reinterpret_cast(cred.get()->CredentialBlob), cred.get()->CredentialBlobSize}}; } void WinCredStorage::Erase(const std::string& serverAddress)