From bfdd87ce0856c527e2d57ca2f71730a4411cb725 Mon Sep 17 00:00:00 2001 From: Lin Date: Sun, 6 Sep 2026 23:56:56 +0800 Subject: [PATCH] net/quic: complete pending reads on Fast Open response failure A delayed non-200 CONNECT response without FIN can arrive after Fast Open has completed Connect() and an application Read() is pending. Setting the socket state to disconnected does not complete the stream's pending read. Reset the stream and explicitly report the failure to the read callback. Also handle an asynchronous header-read failure without invoking the already-consumed connect callback. Run the read callback last because it can destroy the socket, and exit DoLoop without touching members afterward. Combined from eeb2b8ddc1f59a5cddea133443e880801e86af8c and 153de92c8e32f172d369d697c122593befedabfc in ssharkkky/naiveproxy. --- src/net/quic/quic_proxy_client_socket.cc | 41 ++++++++++++++++++++++-- src/net/quic/quic_proxy_client_socket.h | 10 ++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/net/quic/quic_proxy_client_socket.cc b/src/net/quic/quic_proxy_client_socket.cc index 2cfb627ce8..a8bc2084a6 100644 --- a/src/net/quic/quic_proxy_client_socket.cc +++ b/src/net/quic/quic_proxy_client_socket.cc @@ -326,6 +326,23 @@ int QuicProxyClientSocket::DoLoop(int last_io_result) { rv = DoReadReplyComplete(rv); net_log_.EndEventWithNetErrorCode( NetLogEventType::HTTP_TRANSACTION_TUNNEL_READ_HEADERS, rv); + // If reading the response itself fails after Fast Open returned from + // Connect(), there is no connect_callback_ left to invoke. Successful + // responses continue through the normal response-processing states; + // HTTP status failures are handled in STATE_PROCESS_RESPONSE_CODE. + if (use_fastopen_ && read_headers_pending_ && rv < 0) { + read_headers_pending_ = false; + // The pending application read observes the closed stream. Any + // subsequent data after this response must be ignored. + next_state_ = STATE_DISCONNECTED; + // A Fast Open Connect() can complete before the app issues a + // Read(); a pending read must observe this failure instead of + // waiting for a stream close that may never come. + FailPendingReadOnFastOpenFailure(rv); + // The Fast Open Connect() already completed; do not report this + // response through connect_callback_. + rv = ERR_IO_PENDING; + } break; case STATE_PROCESS_RESPONSE_HEADERS: DCHECK_EQ(OK, rv); @@ -340,11 +357,16 @@ int QuicProxyClientSocket::DoLoop(int last_io_result) { if (use_fastopen_ && read_headers_pending_) { read_headers_pending_ = false; if (rv < 0) { - // read_callback_ will be called with this error and be reset. - // Further data after that will be ignored. + // The pending application read observes the closed stream. Any + // subsequent data after this response must be ignored. next_state_ = STATE_DISCONNECTED; + // A Fast Open Connect() can complete before the app issues a + // Read(); a pending read must observe this failure instead of + // waiting for a stream close that may never come. + FailPendingReadOnFastOpenFailure(rv); } - // Prevents calling connect_callback_. + // Fast Open already completed Connect(); do not invoke the + // consumed connect_callback_ for the later response. rv = ERR_IO_PENDING; } break; @@ -602,6 +624,19 @@ int QuicProxyClientSocket::ProcessResponseHeaders( return OK; } +void QuicProxyClientSocket::FailPendingReadOnFastOpenFailure(int error) { + if (read_callback_.is_null()) { + // No pending application read; still cancel the response stream so a + // late body cannot be delivered after the failed CONNECT response. + stream_->Reset(quic::QUIC_STREAM_CANCELLED); + return; + } + read_buf_ = nullptr; + stream_->Reset(quic::QUIC_STREAM_CANCELLED); + // May destroy |this|; run last and use no members afterwards. + std::move(read_callback_).Run(error); +} + void QuicProxyClientSocket::OnBeforeTunnelRequestComplete( base::expected result) { if (result.has_value()) { diff --git a/src/net/quic/quic_proxy_client_socket.h b/src/net/quic/quic_proxy_client_socket.h index 4738a96ea7..3fdeecf669 100644 --- a/src/net/quic/quic_proxy_client_socket.h +++ b/src/net/quic/quic_proxy_client_socket.h @@ -106,6 +106,16 @@ class NET_EXPORT_PRIVATE QuicProxyClientSocket : public ProxyClientSocket { void OnReadResponseHeadersComplete(int result); int ProcessResponseHeaders(const quiche::HttpHeaderBlock& headers); + // Fast Open async failure path: a Fast Open Connect() can complete before + // the app has issued a Read(); when the CONNECT response then fails + // asynchronously, a pending application Read() must observe the failure + // instead of waiting indefinitely for a stream close that may never come. + // Must be called after next_state_ is set to STATE_DISCONNECTED. The read + // callback may destroy |this|, so DoLoop must not touch members afterwards + // (its loop condition short-circuits on the ERR_IO_PENDING the caller sets + // next). + void FailPendingReadOnFastOpenFailure(int error); + // Callback for proxy_delegate_->OnBeforeTunnelRequest(). void OnBeforeTunnelRequestComplete( base::expected result);