Skip to content

AmSession::postDtmfEvent: don't leak refused DTMF events - #567

Merged
hecko merged 2 commits into
masterfrom
hecko/dtmf-event-leak
Sep 13, 2026
Merged

hecko merged 2 commits into
masterfrom
hecko/dtmf-event-leak

Conversation

@hecko

@hecko hecko commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

The bug

AmSession::postDtmfEvent() takes a heap-allocated AmDtmfEvent* and only queues it when m_dtmfDetectionEnabled is true. When detection is disabled it returns without queueing and without deleting — the event leaks. Callers allocate with new and have no way to tell whether ownership was taken.

Three call sites are affected:

Call site Trigger
AmRtpStream::recvDtmfPacket() every inbound RFC 2833 telephone-event RTP packet
AmSession::onSipRequest() every SIP INFO with an application/dtmf-relay body
AmDtmfDetector::reportEvent() aggregated event (not reachable in practice — only runs with detection on)

The RTP path is the serious one: recvDtmfPacket() posts an event for every telephone-event packet with no check on whether the session wants DTMF. One DTMF digit is several RTP packets plus the end-of-event retransmissions, so a remote peer sending DTMF into a session with detection disabled leaks continuously, for the lifetime of the process.

This is not a corner case — disabling DTMF detection is a normal configuration. In-tree users: apps/conference, apps/voicemail, apps/callback, apps/examples/b2b_connect, apps/examples/serviceline, apps/examples/announce_auth, apps/examples/cacheannounce, apps/examples/jukecall, DSM's disableDtmfDetection(), and the IVR binding.

The fix

Make the ownership contract explicit. AmDtmfSink::postDtmfEvent() now returns bool — true when the sink took ownership, false when it refused — and the three call sites release the event when it comes back refused.

AmSession is the only implementation of AmDtmfSink in the tree, so the signature change is contained.

Validation

Full cmake build of core and all apps: clean, no new warnings.

Credit

Backported from sipwise/sems commit 1ec76519 ("AmSession: do not leak heap allocated AmDtmfEvent", MT#59962). Thanks to the Sipwise team for finding and fixing this; adapted to this tree's AmSession/AmRtpStream layout.


Generated by Claude Code

AmSession::postDtmfEvent() silently dropped the heap allocated
AmDtmfEvent it was handed whenever m_dtmfDetectionEnabled is false,
without ever deleting it. The event is allocated by the caller with
new and ownership is only taken over when it is actually queued, so
every refused event leaks.

The RTP path makes this remotely triggerable: AmRtpStream::recvDtmfPacket()
posts an AmRtpDtmfEvent for every telephone-event RTP packet that
arrives, with no check on whether the session wants DTMF at all. A
single DTMF digit is several RTP packets (plus the end retransmissions),
so a peer sending DTMF to a session that disabled detection leaks
steadily for the lifetime of the process. Applications shipped in
this tree disable detection routinely (conference, voicemail,
callback, b2b_connect, serviceline, announce_auth, cacheannounce,
jukecall, DSM's disableDtmfDetection(), IVR), so this is a normal
runtime configuration, not a corner case.

The SIP INFO path (AmSession::onSipRequest) leaks the same way for
application/dtmf-relay bodies.

Make the AmDtmfSink::postDtmfEvent() contract explicit: return true
when the sink took ownership, false when it refused, and release the
event at the three call sites when it comes back refused.
AmSession is the only implementation of AmDtmfSink in the tree.
Copilot AI lite review requested due to automatic review settings September 12, 2026 03:23

hecko commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

deb_test_build is failing here, but not because of this change.

It dies in Dockerfile-debian11 at the very first RUN apt update, before any source is compiled:

E: Release file for http://deb.debian.org/debian-security/dists/bullseye-security/InRelease
   is expired (invalid since 4d 6h 8min 32s)
ERROR: failed to solve: process "/bin/sh -c apt update" did not complete successfully: exit code: 100

Debian 11 (bullseye) reached end of security support, so its InRelease file is no longer being refreshed and apt now rejects it as expired. The same job is already red on master at e5e7e61 for the same reason (run 34616001093), and this PR touches only core/AmSession.*, core/AmDtmfDetector.* and core/AmRtpStream.cpp.

There is no fix for it in the tree yet to carry into this PR. Unblocking it needs a packaging-side change (bump the base image off bullseye, or pass -o Acquire::Check-Valid-Until=false in that Dockerfile), which belongs in its own PR rather than here.

The compile itself is fine: a full cmake build of core plus all apps is clean with no new warnings, and the remaining checks (hardened/asan/ubsan/tsan, FreeBSD 13.5/14.3/15.0, macOS, rpm) are running.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No unresolved blocking issues were identified.

Pull request overview

Fixes leaked heap-allocated DTMF events when detection is disabled by making ownership explicit.

Changes:

  • Returns ownership status from postDtmfEvent().
  • Releases refused SIP, RTP, and detector-generated events.
  • Documents the updated ownership contract.
File summaries
File Description
core/AmSession.h Updates the DTMF API contract.
core/AmSession.cpp Returns ownership status and handles SIP events.
core/AmRtpStream.cpp Releases refused RTP events.
core/AmDtmfDetector.h Updates the sink interface documentation.
core/AmDtmfDetector.cpp Releases refused aggregated events.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Add a test_dtmf suite for the ownership contract introduced by the
previous commit: postDtmfEvent() returns whether the sink took the
event over, and the caller releases what was refused.

- AmDtmfDetector with a stub sink: an accepting sink is handed the
  aggregated event (key and duration), and a refusing sink does not
  wedge the detector, each pending key is still offered exactly once.
- AmSession with detection disabled refuses raw SIP INFO, raw RTP and
  aggregated events, queues nothing, and once detection is enabled
  again only accepted keys reach onDtmf().
- AmSession with detection enabled takes all three kinds: aggregated
  events land in the session's event queue, raw SIP INFO and RTP
  events go through its DTMF detector and come out at onDtmf().
- AmSession::onSipRequest() for an application/dtmf-relay INFO answers
  200 with detection disabled and enabled, and only the INFOs received
  while enabled are delivered. The session runs on an AmSipDialog that
  records replies instead of sending them.

Keys are flushed out of the detector by sending a different key, so
no test waits for the detector timeout.

The release of a refused event at the call sites is not visible to
assertions, because the event is allocated inside the code under test.
A missing delete in AmDtmfDetector::reportEvent() or in
AmSession::onSipRequest() is only reported as a leak by the asan job.
AmRtpStream::recvDtmfPacket() is not covered, since it needs a stream
with a negotiated telephone-event payload type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hecko
hecko merged commit f123b1b into master Sep 13, 2026
25 of 26 checks passed
@hecko
hecko deleted the hecko/dtmf-event-leak branch September 14, 2026 07:22
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.

2 participants