From eddf0605b7ab6df0147c692d5c455de4faeb76c6 Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:56:05 -0700 Subject: [PATCH 1/4] strict crlf --- src/windows/wslcsession/DockerHTTPClient.cpp | 34 ++++++------- src/windows/wslcsession/DockerHTTPClient.h | 51 +++++++++++++++++++- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index 9c855a6d63..b5c14b7bdb 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -661,17 +661,15 @@ void DockerHTTPClient::DockerHttpResponseHandle::OnRead(const gsl::span& C } else { - // Otherwise keep parsing the HTTP response header. + // Otherwise keep parsing the HTTP response header. Scan for the end-of-header marker + // ("\r\n\r\n"); HeaderEnd carries state across reads in case it straddles a boundary. size_t i{}; - for (i = 0; i < Content.size() && LineFeeds < 2; i++) + for (i = 0; i < Content.size(); i++) { - if (Content[i] == '\n') + if (HeaderEnd.Consume(Content[i])) { - LineFeeds++; - } - else if (Content[i] != '\r') - { - LineFeeds = 0; + i++; // Include the terminating byte in the header. + break; } } @@ -805,7 +803,7 @@ std::pair DockerHTTPClient:: parser.eager(false); parser.skip(false); - size_t lineFeeds = 0; + HttpHeaderEndDetector headerEnd; // Consume the socket until the header end is reached while (!parser.is_header_done()) { @@ -820,17 +818,17 @@ std::pair DockerHTTPClient:: THROW_HR_IF(E_ABORT, bytesRead == 0); - // Scan only the newly peeked bytes [Offset, Offset + bytesRead) + // Scan only the newly peeked bytes [Offset, Offset + bytesRead) for the end-of-header + // marker. headerEnd carries state across iterations in case it straddles a read boundary. + bool headerComplete = false; size_t i = 0; - for (i = Offset; i < bytesRead + Offset && lineFeeds < 2; i++) + for (i = Offset; i < bytesRead + Offset; i++) { - if (buffer[i] == '\n') - { - lineFeeds++; - } - else if (buffer[i] != '\r') + if (headerEnd.Consume(buffer[i])) { - lineFeeds = 0; + i++; // Include the terminating byte in the header. + headerComplete = true; + break; } } @@ -847,7 +845,7 @@ std::pair DockerHTTPClient:: Offset += bytesRead; buffer.resize(Offset); - if (lineFeeds == 2) // Header is complete, feed it to the parser. + if (headerComplete) // Header is complete, feed it to the parser. { #ifdef WSLC_HTTP_DEBUG diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index fd24a490bd..01adaf5a0b 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -40,6 +40,55 @@ Module Name: namespace wsl::windows::service::wslc { +// Detects the end-of-header marker ("\r\n\r\n") in an HTTP message. State is maintained +// across successive reads so a terminator split between two reads is handled correctly. +// N.B. Detection is strict: a bare-LF separator ("\n\n") is not treated as a terminator. +class HttpHeaderEndDetector +{ +public: + // Advances the state machine over a single byte. Returns true once the full "\r\n\r\n" + // terminator has been consumed. + bool Consume(char Byte) noexcept + { + switch (m_state) + { + case State::Start: + m_state = (Byte == '\r') ? State::Cr : State::Start; + break; + case State::Cr: + m_state = (Byte == '\n') ? State::CrLf : ((Byte == '\r') ? State::Cr : State::Start); + break; + case State::CrLf: + m_state = (Byte == '\r') ? State::CrLfCr : State::Start; + break; + case State::CrLfCr: + m_state = (Byte == '\n') ? State::Done : ((Byte == '\r') ? State::Cr : State::Start); + break; + case State::Done: + break; + } + + return m_state == State::Done; + } + + bool IsDone() const noexcept + { + return m_state == State::Done; + } + +private: + enum class State + { + Start, + Cr, + CrLf, + CrLfCr, + Done + }; + + State m_state = State::Start; +}; + class DockerHTTPException : public std::runtime_error { public: @@ -199,7 +248,7 @@ class DockerHTTPClient std::function&)> OnResponse; std::function OnCompleted; boost::beast::http::response_parser Parser; - size_t LineFeeds = 0; + HttpHeaderEndDetector HeaderEnd; std::optional RemainingContentLength; std::optional ResponseParser; }; From db93ff12e6b88aa81919656c118d0c183b7f5ffd Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Fri, 26 Jun 2026 14:32:20 -0700 Subject: [PATCH 2/4] improve --- src/windows/wslcsession/DockerHTTPClient.cpp | 26 ++++++-------------- src/windows/wslcsession/DockerHTTPClient.h | 7 ++---- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/windows/wslcsession/DockerHTTPClient.cpp b/src/windows/wslcsession/DockerHTTPClient.cpp index b5c14b7bdb..1ce4728a69 100644 --- a/src/windows/wslcsession/DockerHTTPClient.cpp +++ b/src/windows/wslcsession/DockerHTTPClient.cpp @@ -661,16 +661,11 @@ void DockerHTTPClient::DockerHttpResponseHandle::OnRead(const gsl::span& C } else { - // Otherwise keep parsing the HTTP response header. Scan for the end-of-header marker - // ("\r\n\r\n"); HeaderEnd carries state across reads in case it straddles a boundary. + // Otherwise keep parsing the HTTP response header. size_t i{}; - for (i = 0; i < Content.size(); i++) + for (i = 0; i < Content.size() && !HeaderEnd.IsDone(); i++) { - if (HeaderEnd.Consume(Content[i])) - { - i++; // Include the terminating byte in the header. - break; - } + HeaderEnd.Consume(Content[i]); } // Feed the parser up to the end of the header. @@ -818,18 +813,11 @@ std::pair DockerHTTPClient:: THROW_HR_IF(E_ABORT, bytesRead == 0); - // Scan only the newly peeked bytes [Offset, Offset + bytesRead) for the end-of-header - // marker. headerEnd carries state across iterations in case it straddles a read boundary. - bool headerComplete = false; + // Scan only the newly peeked bytes [Offset, Offset + bytesRead) size_t i = 0; - for (i = Offset; i < bytesRead + Offset; i++) + for (i = Offset; i < bytesRead + Offset && !headerEnd.IsDone(); i++) { - if (headerEnd.Consume(buffer[i])) - { - i++; // Include the terminating byte in the header. - headerComplete = true; - break; - } + headerEnd.Consume(buffer[i]); } WI_ASSERT(i >= Offset); @@ -845,7 +833,7 @@ std::pair DockerHTTPClient:: Offset += bytesRead; buffer.resize(Offset); - if (headerComplete) // Header is complete, feed it to the parser. + if (headerEnd.IsDone()) // Header is complete, feed it to the parser. { #ifdef WSLC_HTTP_DEBUG diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index 01adaf5a0b..783f3d058a 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -40,14 +40,11 @@ Module Name: namespace wsl::windows::service::wslc { -// Detects the end-of-header marker ("\r\n\r\n") in an HTTP message. State is maintained -// across successive reads so a terminator split between two reads is handled correctly. -// N.B. Detection is strict: a bare-LF separator ("\n\n") is not treated as a terminator. +// Detects the end-of-header marker ("\r\n\r\n") in an HTTP message class HttpHeaderEndDetector { public: - // Advances the state machine over a single byte. Returns true once the full "\r\n\r\n" - // terminator has been consumed. + // Returns true once the full "\r\n\r\n" terminator has been consumed. bool Consume(char Byte) noexcept { switch (m_state) From 661e55e439f987cc9a1ab75fe0fd93fc5b88f033 Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:14:34 -0700 Subject: [PATCH 3/4] test --- src/windows/common/HttpHeaderEndDetector.h | 66 ++++++++++++++++++++++ src/windows/wslcsession/DockerHTTPClient.h | 49 +--------------- test/windows/WSLCTests.cpp | 34 +++++++++++ 3 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 src/windows/common/HttpHeaderEndDetector.h diff --git a/src/windows/common/HttpHeaderEndDetector.h b/src/windows/common/HttpHeaderEndDetector.h new file mode 100644 index 0000000000..eb58583e8e --- /dev/null +++ b/src/windows/common/HttpHeaderEndDetector.h @@ -0,0 +1,66 @@ +/*++ + +Copyright (c) Microsoft. All rights reserved. + +Module Name: + + HttpHeaderEndDetector.h + +Abstract: + + This file contains a small state machine that detects the end-of-header + marker ("\r\n\r\n") in an HTTP message, one byte at a time. + +--*/ + +#pragma once + +namespace wsl::windows::common { + +// Detects the end-of-header marker ("\r\n\r\n") in an HTTP message +class HttpHeaderEndDetector +{ +public: + // Returns true once the full "\r\n\r\n" terminator has been consumed. + bool Consume(char Byte) noexcept + { + switch (m_state) + { + case State::Start: + m_state = (Byte == '\r') ? State::Cr : State::Start; + break; + case State::Cr: + m_state = (Byte == '\n') ? State::CrLf : ((Byte == '\r') ? State::Cr : State::Start); + break; + case State::CrLf: + m_state = (Byte == '\r') ? State::CrLfCr : State::Start; + break; + case State::CrLfCr: + m_state = (Byte == '\n') ? State::Done : ((Byte == '\r') ? State::Cr : State::Start); + break; + case State::Done: + break; + } + + return m_state == State::Done; + } + + bool IsDone() const noexcept + { + return m_state == State::Done; + } + +private: + enum class State + { + Start, + Cr, + CrLf, + CrLfCr, + Done + }; + + State m_state = State::Start; +}; + +} // namespace wsl::windows::common diff --git a/src/windows/wslcsession/DockerHTTPClient.h b/src/windows/wslcsession/DockerHTTPClient.h index 783f3d058a..504c9c29a5 100644 --- a/src/windows/wslcsession/DockerHTTPClient.h +++ b/src/windows/wslcsession/DockerHTTPClient.h @@ -20,6 +20,7 @@ Module Name: #include #include "relay.hpp" #include "docker_schema.h" +#include "HttpHeaderEndDetector.h" #define THROW_DOCKER_USER_ERROR_MSG(_Ex, _Msg, ...) \ if ((_Ex).HasErrorMessage()) \ @@ -40,52 +41,6 @@ Module Name: namespace wsl::windows::service::wslc { -// Detects the end-of-header marker ("\r\n\r\n") in an HTTP message -class HttpHeaderEndDetector -{ -public: - // Returns true once the full "\r\n\r\n" terminator has been consumed. - bool Consume(char Byte) noexcept - { - switch (m_state) - { - case State::Start: - m_state = (Byte == '\r') ? State::Cr : State::Start; - break; - case State::Cr: - m_state = (Byte == '\n') ? State::CrLf : ((Byte == '\r') ? State::Cr : State::Start); - break; - case State::CrLf: - m_state = (Byte == '\r') ? State::CrLfCr : State::Start; - break; - case State::CrLfCr: - m_state = (Byte == '\n') ? State::Done : ((Byte == '\r') ? State::Cr : State::Start); - break; - case State::Done: - break; - } - - return m_state == State::Done; - } - - bool IsDone() const noexcept - { - return m_state == State::Done; - } - -private: - enum class State - { - Start, - Cr, - CrLf, - CrLfCr, - Done - }; - - State m_state = State::Start; -}; - class DockerHTTPException : public std::runtime_error { public: @@ -245,7 +200,7 @@ class DockerHTTPClient std::function&)> OnResponse; std::function OnCompleted; boost::beast::http::response_parser Parser; - HttpHeaderEndDetector HeaderEnd; + common::HttpHeaderEndDetector HeaderEnd; std::optional RemainingContentLength; std::optional ResponseParser; }; diff --git a/test/windows/WSLCTests.cpp b/test/windows/WSLCTests.cpp index 8baad20ab4..0f00f536ac 100644 --- a/test/windows/WSLCTests.cpp +++ b/test/windows/WSLCTests.cpp @@ -22,6 +22,7 @@ Module Name: #include "hcs.hpp" #include "ContainerNameGenerator.h" #include "wslc/e2e/WSLCE2EHelpers.h" +#include "HttpHeaderEndDetector.h" #include using namespace std::literals::chrono_literals; @@ -9220,6 +9221,39 @@ class WSLCTests VERIFY_IS_TRUE(payload == output); } + TEST_METHOD(HttpHeaderEndDetector) + { + // Returns the index of the byte of header end, or -1 if the header never ends. + const auto headerEndIndex = [](std::string_view input) { + wsl::windows::common::HttpHeaderEndDetector detector; + for (size_t i = 0; i < input.size(); i++) + { + if (detector.Consume(input[i])) + { + return static_cast(i); + } + } + + return -1; + }; + + VERIFY_ARE_EQUAL(3, headerEndIndex("\r\n\r\n")); + VERIFY_ARE_EQUAL(4, headerEndIndex("a\r\n\r\n")); + VERIFY_ARE_EQUAL(7, headerEndIndex("a\r\nb\r\n\r\n")); + VERIFY_ARE_EQUAL(4, headerEndIndex("\r\r\n\r\n")); + VERIFY_ARE_EQUAL(3, headerEndIndex("\r\n\r\nbody")); + + VERIFY_ARE_EQUAL(-1, headerEndIndex("")); + VERIFY_ARE_EQUAL(-1, headerEndIndex("Header: value\r\n")); + VERIFY_ARE_EQUAL(-1, headerEndIndex("HTTP/1.1 200 OK\r\n")); + VERIFY_ARE_EQUAL(-1, headerEndIndex("\r\n\r")); + + // Detection is strict. + VERIFY_ARE_EQUAL(-1, headerEndIndex("\n\n")); + VERIFY_ARE_EQUAL(-1, headerEndIndex("\r\n\n")); + VERIFY_ARE_EQUAL(-1, headerEndIndex("\n\r\n")); + } + WSLC_TEST_METHOD(ContainerRecoveryFromStorage) { auto restore = ResetTestSession(); // Required to access the storage folder. From d5127da1bdf425a5465de6cd88778cdaa1bea737 Mon Sep 17 00:00:00 2001 From: yao-msft <50888816+yao-msft@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:03:34 -0700 Subject: [PATCH 4/4] case --- src/windows/common/HttpHeaderEndDetector.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/windows/common/HttpHeaderEndDetector.h b/src/windows/common/HttpHeaderEndDetector.h index eb58583e8e..1a5003102a 100644 --- a/src/windows/common/HttpHeaderEndDetector.h +++ b/src/windows/common/HttpHeaderEndDetector.h @@ -22,21 +22,21 @@ class HttpHeaderEndDetector { public: // Returns true once the full "\r\n\r\n" terminator has been consumed. - bool Consume(char Byte) noexcept + bool Consume(char byte) noexcept { switch (m_state) { case State::Start: - m_state = (Byte == '\r') ? State::Cr : State::Start; + m_state = (byte == '\r') ? State::Cr : State::Start; break; case State::Cr: - m_state = (Byte == '\n') ? State::CrLf : ((Byte == '\r') ? State::Cr : State::Start); + m_state = (byte == '\n') ? State::CrLf : ((byte == '\r') ? State::Cr : State::Start); break; case State::CrLf: - m_state = (Byte == '\r') ? State::CrLfCr : State::Start; + m_state = (byte == '\r') ? State::CrLfCr : State::Start; break; case State::CrLfCr: - m_state = (Byte == '\n') ? State::Done : ((Byte == '\r') ? State::Cr : State::Start); + m_state = (byte == '\n') ? State::Done : ((byte == '\r') ? State::Cr : State::Start); break; case State::Done: break;