Skip to content

UDP throttler deadlock — sendLocker held when signaling QueueForSendingControlPacket

Historical reference only: stale-v0.72a-experimental-clean and analysis\stale-v0.72a-experimental-clean are retired reference sources, not active branch targets or current baselines. Use them only as provenance or idea-extraction sources; landed status is determined against main. See Historical References.

Summary

CClientUDPSocket::OnDatagramSend() calls QueueForSendingControlPacket() while holding sendLocker (CCriticalSection). The upload bandwidth throttler (CUploadBandwidthThrottler) can call back into the UDP socket from its own thread, also acquiring sendLocker. This creates a classic lock-order inversion deadlock:

  • Thread A (socket): holds sendLocker, waits for throttler queue lock
  • Thread B (throttler): holds throttler queue lock, waits for sendLocker via SendControlData

Additionally, SendPacket() held sendLocker across the QueueForSendingControlPacket() call, extending the window unnecessarily.

Location

srchybrid/ClientUDPSocket.cpp

// BEFORE — sendLocker held when signaling throttler (OnDatagramSend):
CSingleLock lockSend(&sendLocker, TRUE);
m_bWouldBlock = false;
SetWriteInterestEnabled(false);
if (ShouldSignalUdpControlQueue(m_bWouldBlock, controlpacket_queue.IsEmpty()))
    theApp.uploadBandwidthThrottler->QueueForSendingControlPacket(this);  // DEADLOCK

// BEFORE — sendLocker held when signaling throttler (SendPacket):
CSingleLock lockSend(&sendLocker, TRUE);
controlpacket_queue.AddTail(newpending);
theApp.uploadBandwidthThrottler->QueueForSendingControlPacket(this);  // DEADLOCK

Experimental Reference Implementation

Status in stale-v0.72a-experimental-clean: Fixed in commit 5a0ab27 (CPP_022 CPP_026, 2026-04-02).

Fix: 1. OnDatagramSend: capture the signal decision under lock, then release before calling the throttler: cpp const bool bShouldSignalQueue = ShouldSignalUdpControlQueue(m_bWouldBlock, controlpacket_queue.IsEmpty()); lockSend.Unlock(); if (bShouldSignalQueue) theApp.uploadBandwidthThrottler->QueueForSendingControlPacket(this); 2. SendPacket: scope sendLocker to only cover controlpacket_queue.AddTail(), release before calling the throttler: cpp { CSingleLock lockSend(&sendLocker, TRUE); controlpacket_queue.AddTail(newpending); } theApp.uploadBandwidthThrottler->QueueForSendingControlPacket(this);

Files changed in experimental: srchybrid/ClientUDPSocket.cpp, srchybrid/UploadBandwidthThrottler.cpp, srchybrid/UploadBandwidthThrottlerSeams.h (+36/-14 lines).

Current Mainline Status

Done in main via commit 6cf4967 (Fix UDP throttler lock inversion in UDP sockets).

The landed fix follows the same narrow shape as the experimental reference:

  • decide whether the throttler needs a wake-up while sendLocker is held
  • release sendLocker
  • only then call QueueForSendingControlPacket(...)

Porting Note

The fix is clean and isolated to ClientUDPSocket.cpp. Port the two lock-scope adjustments. The seam header (UploadBandwidthThrottlerSeams.h) is test infrastructure and optional. Verify with CUploadBandwidthThrottler::QueueForSendingControlPacket to confirm no lock inversion remains.

Severity

Deadlock is not guaranteed to manifest on every run — it requires a race between the throttler thread calling back into the socket during its own send loop and OnDatagramSend/SendPacket executing simultaneously. Under high UDP load it becomes reproducible.