dialog: fix refcount race between BYE and timer expiry#3836
Conversation
b5b39c3 to
196b51f
Compare
42c3041 to
1c2d0bd
Compare
…3835) The previous fix (196b51f) narrowed the race window between BYE processing and dlg_ontimeout() but could not fully close it: a BYE arriving between the state check and dlg_end_dlg() creates a TM transaction ref (via dlg_set_tm_dialog_ctx) that outlives the dialog. When the timeout-BYE responses destroy the dialog first, the real BYE's transaction callback dereferences freed memory. The same race pattern exists in dlg_options_routine() and dlg_reinvite_routine(), where expired ping dialogs call dlg_end_dlg() without any state transition. Fix: replace racy state checks with atomic next_state_dlg(DLG_EVENT_REQBYE) calls. The hash entry lock guarantees only one code path -- timer, ping handler, or BYE handler -- wins the CONFIRMED -> DELETED transition. Since dual_bye_event() now only sees DELETED -> DELETED for the timeout-BYE responses, the cleanup that it would normally perform on the first CONFIRMED -> DELETED transition (rt_on_hangup, profile linkers, DLGCB_TERMINATED callback, DB removal) is moved into the winning timeout/ping path. Three paths fixed: - dlg_ontimeout() bye_on_timeout (dlg_handlers.c) - dlg_options_routine() expired pings (dlg_timer.c) - dlg_reinvite_routine() expired pings (dlg_timer.c) Ref: OpenSIPS#3835
1c2d0bd to
2c285de
Compare
|
Most likely this is a side effect.
Should be closed as invalid for now.
…On Wed, Jun 10, 2026 at 01:49 Bogdan Andrei IANCU ***@***.***> wrote:
*bogdan-iancu* left a comment (OpenSIPS/opensips#3836)
<#3836 (comment)>
@NormB <https://github.com/NormB> , somehow I thing this is related to
the locking issue (on aarm64) which was sorted out in #3835
<#3835>. With the fix in
place, can you re-check if the report here is still valid ? Thanks!
—
Reply to this email directly, view it on GitHub
<#3836?email_source=notifications&email_token=ABA2XP64E37XHXAVTPR3CWD47DZFNA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINRWGY4TMNZYGQ42M4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSWGM33PORSXEX3DNRUWG2Y#issuecomment-4666967849>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABA2XP2CHM7PFBRMKIFAAXL47DZFNAVCNFSM6AAAAACWITGC3CVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DMNRWHE3DOOBUHE>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/ABA2XP3BFRRD4AK7DSMKTOL47DZFNA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINRWGY4TMNZYGQ42M4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSVGM33PORSXEX3JN5ZQ>
and Android
<https://github.com/notifications/mobile/android/ABA2XP6VNCXYJJMVO3DU7PL47DZFNA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINRWGY4TMNZYGQ42M4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSXGM33PORSXEX3BNZSHE33JMQ>.
Download it today!
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
|
@NormB , let me know if you confirm that the problem disappeared in the latest git code, without the need of this patch here ? |
|
I went back and re-tested against current master, and I can't reproduce it anymore without this patch. I built master at tip (which now includes the aarch64 FAST_LOCK work from #3892) and ran a focused reproduction of the original scenario: create_dialog("bye-on-timeout") with a short dialog timeout, the UAS retransmitting the 200 OK, and a BYE arriving right as the dialog times out — i.e. the exact dlg_ontimeout vs BYE window, on aarch64 under load. On the older code this would SIGSEGV in dlg_release_cloned_leg within a minute or two; on current master it stays up across a sustained soak with no bogus ref and no crash. So I agree with @ovidiusas — this looks like it was a side effect of the locking issue that #3835 fixed, and it's no longer reproducible on its own. I'm fine closing this PR as invalid; happy to revisit if anyone sees the signature resurface on a current build. |
|
Perfect, thank you @NormB ! |
Summary
Fix use-after-free crash caused by a race between BYE processing and
dlg_ontimeout()whenbye_on_timeoutis enabled (create_dialog("B")). Reproduced on ARM64 (aarch64) — matches the crash signature reported in #3835 (bogus ref -1/ SIGSEGV indlg_release_cloned_leg).Details
Two independent race paths exist between BYE processing and the dialog timer handler:
Race Path 1 — Timer removal outside lock:
next_state_dlg()transitions the dialog toDELETEDand releases the hash entry lock before the caller performsremove_dlg_timer(). A concurrent worker can observestate=DELETEDand begin its own cleanup, racing with the caller's timer removal.Race Path 2 — Stale state read in timer handler:
dlg_ontimeout()readsdlg->stateat three locations without the hash entry lock (dlg_handlers.c:2468,dlg_handlers.c:2491,dlg_handlers.c:2518). On ARM64's relaxed memory ordering, these reads can return a staleCONFIRMEDvalue after a BYE worker has already setDELETEDunder the lock. The timer handler then enters thebye_on_timeoutpath on a dialog that is already being torn down.Both races are architecture-sensitive — ARM64's relaxed memory ordering makes them significantly more likely than on x86_64. This is consistent with the reporter seeing crashes on Graviton while the issue is difficult to reproduce on x86.
Race window diagram
The same timer-removal-outside-lock pattern exists in all four callers of
next_state_dlg():dlg_handlers.cdlg_onroute()dlg_req_within.cdual_bye_event()dlg_replication.cdlg_replicated_delete()dlg_replication.cdrop_dlg()Solution
A two-part fix. Our testing indicates neither part alone is sufficient — both are needed to cover all race windows.
Part 1: Timer removal inside lock (
dlg_hash.c): Moveremove_dlg_timer()insidenext_state_dlg()'s lock scope. When transitioning toDELETED, the timer is removed atomically with the state change. All four callers' post-next_state_dlg()remove_dlg_timer()blocks are removed sincenext_state_dlg()now handles this centrally.Lock ordering note:
remove_dlg_timer()acquiresd_timer->lockinternally. The resulting hash entry lock → timer lock ordering is already established by existing code paths (e.g.,insert_dlg_timer()called while holding the hash lock).Part 2: Locked state read in
dlg_ontimeout()(dlg_handlers.c): Readdlg->stateunder the hash entry lock into a localdlg_statevariable, then usedlg_statefor all subsequent state-dependent decisions. The lock acquire/release provides the memory barriers needed for ARM64 visibility.Why both parts are needed
Part 1 alone was applied and tested on our ARM64 test VM — it still crashed within ~2 minutes. The reason: when the timer has already been dequeued by the timer wheel before the BYE arrives,
remove_dlg_timer()returns>0(not0), so Part 1's(*unref)++does not execute. The timer handler then proceeds with a staleCONFIRMEDstate and consumes the ref viaunref_dlg(dlg, 1), leading to premature destruction.Part 2 alone doesn't cover the window where the timer hasn't fired yet — a concurrent worker can see
state=DELETEDand race to unref before the timer ref has been accounted for.ARM64 reproduction results
Reproduced on QEMU aarch64 VM (Debian 12, 2 vCPUs, 2GB RAM) using unmodified OpenSIPS master source (commit
aad6b85). No simulation code or injected delays — crashes occurred naturally under load.Test methodology: high worker count (32) on 2 vCPUs, short dialog timeout (2s) with SIPp BYE timed ~100ms before timeout, stress-ng for CPU/memory/IO pressure, taskset pinning timer + workers to same CPU core.
Crash backtraces from unpatched runs:
GDB inspection of the Part-1-only crash confirmed use-after-free: the dialog at the crash address had
state=EARLY(1), ref=2— a completely different dialog occupying reused shared memory.Important caveat: these results are from a QEMU-emulated ARM64 environment, not production hardware. The crashes are consistent with the reported issue, but testing on production ARM64 hardware under real traffic patterns would provide additional confidence.
Refcount walkthrough (normal BYE, pre-fix crash, post-fix safe)
Normal BYE (with fix):
BYE + Timer race (current code, crash):
BYE + Timer race (with fix, safe):
Compatibility
No behavioral changes to existing call flows. The fix centralizes timer removal that was previously done by each caller, and adds a lock/read/unlock at the top of
dlg_ontimeout(). The lock ordering (hash entry → timer) is already established in the existing codebase. No configuration changes or migration needed.Note: the existing
DLG_FLAG_RACE_CONDITION_OCCURREDmechanism (SIP-level CANCEL/200-OK race viacreate_dialog("E")) is a different race at the signaling level. This fix protects that code path as well since it also flows throughdlg_ontimeout().Closing issues
Closes #3835