From 245df63f683017db2dbd7edaf2332b744444ef2c Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:34:24 -0700 Subject: [PATCH 1/3] Add test covergae for LoadImage/ImportImage on process exit/session terminate --- test/windows/WSLCTests.cpp | 126 +++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 5f5df349b..ebc6103d9 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -887,6 +887,68 @@ class WSLCTests ValidateCOMErrorMessage(L"archive/tar: invalid tar header"); } + + // Validate that LoadImage is aborted when the input data source pipe is broken and the session terminates. + // This simulates the scenario where the calling process exits during a LoadImage operation. + { + wil::unique_handle pipeRead; + wil::unique_handle pipeWrite; + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2)); + + std::promise processExitResult; + wil::unique_event testCompleted{wil::EventOptions::ManualReset}; + std::thread operationThread([&]() { + processExitResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024)); + WI_ASSERT(testCompleted.is_signaled()); + }); + + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); }); + + // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes). + DWORD bytesWritten{}; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr)); + + testCompleted.SetEvent(); + + // Close the write end of the pipe to simulate the input process exiting, + // then terminate the session to cancel the synchronous IO in the service. + pipeWrite.reset(); + VERIFY_SUCCEEDED(m_defaultSession->Terminate()); + + auto restore = ResetTestSession(); + + auto hr = processExitResult.get_future().get(); + VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED)); + } + + // Validate that LoadImage is aborted when the session terminates. + { + wil::unique_handle pipeRead; + wil::unique_handle pipeWrite; + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2)); + + std::promise terminateResult; + wil::unique_event testCompleted{wil::EventOptions::ManualReset}; + std::thread operationThread([&]() { + terminateResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024)); + WI_ASSERT(testCompleted.is_signaled()); + }); + + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); }); + + // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes). + DWORD bytesWritten{}; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr)); + + testCompleted.SetEvent(); + + VERIFY_SUCCEEDED(m_defaultSession->Terminate()); + + auto restore = ResetTestSession(); + + auto hr = terminateResult.get_future().get(); + VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED)); + } } WSLC_TEST_METHOD(ImportImage) @@ -936,6 +998,70 @@ class WSLCTests ValidateCOMErrorMessage(L"archive/tar: invalid tar header"); } + + // Validate that ImportImage is aborted when the input data source pipe is broken and the session terminates. + // This simulates the scenario where the calling process exits during an ImportImage operation. + { + wil::unique_handle pipeRead; + wil::unique_handle pipeWrite; + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2)); + + std::promise processExitResult; + wil::unique_event testCompleted{wil::EventOptions::ManualReset}; + std::thread operationThread([&]() { + processExitResult.set_value( + m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "process-exit:test", nullptr, 1024 * 1024)); + WI_ASSERT(testCompleted.is_signaled()); + }); + + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); }); + + // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes). + DWORD bytesWritten{}; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr)); + + testCompleted.SetEvent(); + + // Close the write end of the pipe to simulate the input process exiting, + // then terminate the session to cancel the synchronous IO in the service. + pipeWrite.reset(); + VERIFY_SUCCEEDED(m_defaultSession->Terminate()); + + auto restore = ResetTestSession(); + + auto hr = processExitResult.get_future().get(); + VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED)); + } + + // Validate that ImportImage is aborted when the session terminates. + { + wil::unique_handle pipeRead; + wil::unique_handle pipeWrite; + VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2)); + + std::promise terminateResult; + wil::unique_event testCompleted{wil::EventOptions::ManualReset}; + std::thread operationThread([&]() { + terminateResult.set_value( + m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "session-terminate:test", nullptr, 1024 * 1024)); + WI_ASSERT(testCompleted.is_signaled()); + }); + + auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); }); + + // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes). + DWORD bytesWritten{}; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr)); + + testCompleted.SetEvent(); + + VERIFY_SUCCEEDED(m_defaultSession->Terminate()); + + auto restore = ResetTestSession(); + + auto hr = terminateResult.get_future().get(); + VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED)); + } } WSLC_TEST_METHOD(DeleteImage) From eafa668db6069c519c69467d119536cca9daff82 Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:12:52 -0700 Subject: [PATCH 2/3] fix handle read interupt --- src/windows/wslcsession/WSLCSession.cpp | 9 ++++- test/windows/WSLCTests.cpp | 45 +++++++------------------ 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/windows/wslcsession/WSLCSession.cpp b/src/windows/wslcsession/WSLCSession.cpp index f82c4af73..e9d4f5c42 100644 --- a/src/windows/wslcsession/WSLCSession.cpp +++ b/src/windows/wslcsession/WSLCSession.cpp @@ -912,8 +912,15 @@ void WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, } }; + // Shutdown the Docker stream's write side when the user pipe is closed. + // This is required for Docker to know when the request body is complete. + auto onInputComplete = [socket = Request.stream.native_handle()]() { + LOG_LAST_ERROR_IF(shutdown(socket, SD_SEND) == SOCKET_ERROR); + }; + io.AddHandle(std::make_unique>( - userHandle.Get(), common::relay::HandleWrapper{Request.stream.native_handle()})); + common::relay::HandleWrapper{userHandle.Get(), std::move(onInputComplete)}, + common::relay::HandleWrapper{Request.stream.native_handle()})); io.AddHandle( std::make_unique(Request, std::move(onHttpResponse), std::move(onProgress)), diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 2b3bedb38..5b33243ea 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -1001,37 +1001,27 @@ class WSLCTests ValidateCOMErrorMessage(L"archive/tar: invalid tar header"); } - // Validate that LoadImage is aborted when the input data source pipe is broken and the session terminates. - // This simulates the scenario where the calling process exits during a LoadImage operation. + // Validate that LoadImage fails when the input pipe is closed during reading. { wil::unique_handle pipeRead; wil::unique_handle pipeWrite; VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2)); - std::promise processExitResult; - wil::unique_event testCompleted{wil::EventOptions::ManualReset}; + std::promise loadResult; std::thread operationThread([&]() { - processExitResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024)); - WI_ASSERT(testCompleted.is_signaled()); + loadResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024)); }); auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); }); - // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes). + // Write some data to ensure the service has started reading from the pipe (pipe buffer is 2 bytes). DWORD bytesWritten{}; VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr)); - testCompleted.SetEvent(); - - // Close the write end of the pipe to simulate the input process exiting, - // then terminate the session to cancel the synchronous IO in the service. + // Close the write end. pipeWrite.reset(); - VERIFY_SUCCEEDED(m_defaultSession->Terminate()); - - auto restore = ResetTestSession(); - auto hr = processExitResult.get_future().get(); - VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED)); + VERIFY_ARE_EQUAL(E_FAIL, loadResult.get_future().get()); } // Validate that LoadImage is aborted when the session terminates. @@ -1112,38 +1102,27 @@ class WSLCTests ValidateCOMErrorMessage(L"archive/tar: invalid tar header"); } - // Validate that ImportImage is aborted when the input data source pipe is broken and the session terminates. - // This simulates the scenario where the calling process exits during an ImportImage operation. + // Validate that ImportImage fails when the input pipe is closed during reading. { wil::unique_handle pipeRead; wil::unique_handle pipeWrite; VERIFY_WIN32_BOOL_SUCCEEDED(CreatePipe(&pipeRead, &pipeWrite, nullptr, 2)); - std::promise processExitResult; - wil::unique_event testCompleted{wil::EventOptions::ManualReset}; + std::promise importResult; std::thread operationThread([&]() { - processExitResult.set_value( - m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "process-exit:test", nullptr, 1024 * 1024)); - WI_ASSERT(testCompleted.is_signaled()); + importResult.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "broken-read:eof", nullptr, 1024 * 1024)); }); auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); }); - // Write some data to validate that the service has started reading from the pipe (pipe buffer is 2 bytes). + // Write some data to ensure the service has started reading from the pipe (pipe buffer is 2 bytes). DWORD bytesWritten{}; VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(pipeWrite.get(), "data", 4, &bytesWritten, nullptr)); - testCompleted.SetEvent(); - - // Close the write end of the pipe to simulate the input process exiting, - // then terminate the session to cancel the synchronous IO in the service. + // Close the write end. pipeWrite.reset(); - VERIFY_SUCCEEDED(m_defaultSession->Terminate()); - - auto restore = ResetTestSession(); - auto hr = processExitResult.get_future().get(); - VERIFY_IS_TRUE(hr == E_ABORT || hr == HRESULT_FROM_WIN32(ERROR_OPERATION_ABORTED)); + VERIFY_ARE_EQUAL(E_FAIL, importResult.get_future().get()); } // Validate that ImportImage is aborted when the session terminates. From 09d692416f30365df59f5d78d75f87216135883f Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:01:24 -0700 Subject: [PATCH 3/3] do not leak container --- test/windows/WSLCTests.cpp | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 5b33243ea..ef1f3130f 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -983,13 +983,17 @@ class WSLCTests // Verify that the image is in the list of images. ExpectImagePresent(*m_defaultSession, "hello-world:latest"); - WSLCContainerLauncher launcher("hello-world:latest", "wslc-load-image-container"); - auto container = launcher.Launch(*m_defaultSession); - auto result = container.GetInitProcess().WaitAndCaptureOutput(); + // Validate container launch from the loaded image + { + WSLCContainerLauncher launcher("hello-world:latest", "wslc-load-image-container"); - VERIFY_ARE_EQUAL(0, result.Code); - VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos); + auto container = launcher.Launch(*m_defaultSession); + auto result = container.GetInitProcess().WaitAndCaptureOutput(); + + VERIFY_ARE_EQUAL(0, result.Code); + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos); + } // Validate that invalid tars fail with proper error message and code. { @@ -1073,13 +1077,15 @@ class WSLCTests ExpectImagePresent(*m_defaultSession, "my-hello-world:test"); // Validate that containers can be started from the imported image. - WSLCContainerLauncher launcher("my-hello-world:test", "wslc-import-image-container", {"/hello"}); + { + WSLCContainerLauncher launcher("my-hello-world:test", "wslc-import-image-container", {"/hello"}); - auto container = launcher.Launch(*m_defaultSession); - auto result = container.GetInitProcess().WaitAndCaptureOutput(); + auto container = launcher.Launch(*m_defaultSession); + auto result = container.GetInitProcess().WaitAndCaptureOutput(); - VERIFY_ARE_EQUAL(0, result.Code); - VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos); + VERIFY_ARE_EQUAL(0, result.Code); + VERIFY_IS_TRUE(result.Output[1].find("Hello from Docker!") != std::string::npos); + } // Validate that ImportImage fails if no tag is passed {