UDP throttler deadlock — sendLocker held when signaling QueueForSendingControlPacket
Historical reference only:
stale-v0.72a-experimental-cleanandanalysis\stale-v0.72a-experimental-cleanare retired reference sources, not active branch targets or current baselines. Use them only as provenance or idea-extraction sources; landed status is determined againstmain. 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
sendLockerviaSendControlData
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
sendLockeris 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.