Skip to content

ltc: fix NodeRPC::Send UAF (thread-race on shared HTTP client) — .157 crash-loop ship-blocker - #270

Merged
frstrtr merged 1 commit into
masterfrom
ltc-doge/noderpc-send-mutex-uaf
Jun 20, 2026
Merged

ltc: fix NodeRPC::Send UAF (thread-race on shared HTTP client) — .157 crash-loop ship-blocker#270
frstrtr merged 1 commit into
masterfrom
ltc-doge/noderpc-send-mutex-uaf

Conversation

@frstrtr

@frstrtr frstrtr commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Ship-blocker: .157 v36-crossing crash-loop, root-caused

The .157 soak SEGV-loop (~every 25-50 min during reorg/clean churn, inside malloc) is not a sharechain reorg-prune bug as earlier hypothesized. A firewall-isolated ASan sibling (BuildId 9a12b6bc) captured a definitive heap-use-after-free:

heap-use-after-free, READ size 21 in read_iovec (boost::beast http::write), thread T8
  T8 (asio thread_pool): NodeImpl::clean_tracker -> ShareTracker::think (share_tracker.hpp:1122)
     -> score (share_tracker.hpp:622) -> score-callback (c2pool_refactored.cpp:2650)
     -> NodeRPC::getblockheader (rpc.cpp:440) -> NodeRPC::Send (rpc.cpp:139)
  freed by T0 (main): NodeRPC::Send -> prepare_payload -> set_content_length_impl
     -> delete_element  (frees the Content-Length field element T8 is still serializing)

Root cause

ltc::coin::NodeRPC holds a single shared m_http_request + m_stream and Send() had no mutex. The asio pool worker (clean_tracker -> think -> score fires a blocking getblockheader) and the main thread both drive Send() on the same client. One thread mid-http::write while the other re-runs prepare_payload() frees/reallocs the header element under it -> UAF. In the non-ASan build this is the SEGV-inside-malloc crash-loop (heap corruption is the downstream symptom). Crossing churn just raises clean_tracker frequency, widening the window — which is why it looked crossing-specific.

Fix

Serialize the full write+read cycle in Send() under a per-client std::mutex. A single HTTP connection cannot interleave two request/response pairs anyway, so the lock adds no stall beyond what correctness already requires. +7 lines, no behavior change on the single-threaded path.

Verification

  • Builds clean (c2pool target, exit 0).
  • Next: rebuild .157, re-run the v35->v36 crossing under the same ASan sibling to confirm the UAF is gone.

Follow-up (separate PR, non-blocking on cutover)

Get think()/score() off the synchronous-RPC-on-pool-thread path: cache header height instead of a live getblockheader in the score callback. Also consider guarding the reconnect-timer path against Send() (rarer race on m_stream).

NodeRPC holds a single shared m_http_request + m_stream and Send() drove
them with no synchronization. The asio thread_pool worker path
(clean_tracker -> ShareTracker::think -> score -> score-callback ->
NodeRPC::getblockheader -> Send) issues a blocking RPC concurrently with
the main thread's own RPC traffic on the same client.

ASan (firewall-isolated sibling, BuildId 9a12b6bc) caught the resulting
heap-use-after-free: while one thread is mid http::write serializing the
request fields (read_iovec), the other enters Send -> prepare_payload ->
set_content_length_impl -> delete_element and frees the Content-Length
basic_fields element the first thread is still reading. In the non-ASan
build this surfaces as the .157 SEGV-inside-malloc crash-loop during
reorg/clean churn (the heap corruption was the downstream symptom).

Guard the full write+read cycle with a per-client mutex. A single HTTP
connection cannot interleave two request/response pairs anyway, so this
adds no stall beyond what correctness already requires.

Follow-up (separate change): keep think()/score() off the synchronous
RPC-on-pool-thread path entirely (cache header height instead of a live
getblockheader in the score callback).
@frstrtr
frstrtr merged commit 67d1fb1 into master Jun 20, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant