Skip to content

receiver: fix idle-reset use-after-free and re-latch feedback on mid-session source change#74

Open
o2d2-2d2o wants to merge 2 commits into
EricssonResearch:masterfrom
o2d2-2d2o:receiver-cgnat-relatch
Open

receiver: fix idle-reset use-after-free and re-latch feedback on mid-session source change#74
o2d2-2d2o wants to merge 2 commits into
EricssonResearch:masterfrom
o2d2-2d2o:receiver-cgnat-relatch

Conversation

@o2d2-2d2o

Copy link
Copy Markdown

Two receiver robustness fixes, both surfaced running scream_bw_test_rx in
learn-from-source (reply-to-source) mode against senders on cellular/CGNAT
links. Both are independent commits on top of current master.

1. Use-after-free on the idle reset (ac45e78)

When no RTP arrives for >2 s the receive loop does
delete screamRx; screamRx = new ScreamRx(...) to reset state. The pointer
swap and the periodic RTCP thread's reads of screamRx (its feedback-decision
condition) were both unlocked. A sender on a flapping link (repeated >2 s
stalls then resume, e.g. a probe losing cellular coverage) can drive the reset
repeatedly until the periodic thread dereferences freed memory and crashes the
receiver with SIGSEGV.

Fix, all under the existing lock_scream:

  • take the lock across the delete/new swap;
  • evaluate the periodic thread's feedback decision and build the feedback
    packet under the lock, keeping sendto outside the critical section;
  • move pthread_mutex_init ahead of pthread_create (the periodic thread now
    locks on every iteration, so the mutex must be valid before it starts).

2. Re-latch feedback target on a mid-session source change (72e2cdc)

In learn-from-source mode the receiver latched the sender's address from the
first RTP packet and never revised it. When the sender's public NAT mapping
moves mid-session (routine on CGNAT after tens of minutes), feedback kept going
to the stale address, so the sender received no congestion feedback and its
rate controller ran blind — throughput collapses toward minrate.

Fix: re-latch outgoing_rtcp_addr(6) whenever the observed RTP source ip:port
changes, gated strictly to learn-from-source mode so a seeded receiver
(explicit sender_ip) never changes its target. A new same_peer() helper
compares family + address + port. The periodic feedback thread now snapshots
the target under lock_scream before sendto, since the receive thread may
rewrite it concurrently.

Testing

Builds clean (no warnings) with cmake + make on gcc:13. Verified in
production: a receiver whose feedback was stranded at a stale CGNAT mapping
(~2.5 Mbit/s, pinned near minrate) recovered to ~23 Mbit/s once the source
change was re-latched.

onkoonko added 2 commits July 8, 2026 11:33
When no RTP arrives for >2 s, the main loop deletes and recreates the
global ScreamRx instance to reset receiver state. The pointer swap ran
with no lock held, while rtcpPeriodicThread concurrently dereferences
the same pointer (getRtcpFbInterval/isFeedback/checkIfFlushAck/
getLastFeedbackT in its feedback-decision condition, evaluated before it
acquired lock_scream). If the periodic thread touched screamRx in the
window between delete and the new assignment, it read freed memory and
the receiver crashed with SIGSEGV.

This is easy to hit when the RTP source flaps: a sender behind a flaky
cellular/NAT link that repeatedly stalls >2 s then resumes drives the
idle reset over and over, eventually losing the race.

Fix, all under the existing lock_scream:
- hold the lock across the delete/new pointer swap in the idle-reset path;
- evaluate the periodic thread's feedback condition and build the
  feedback packet under the lock (keeping the sendto network I/O outside
  it, matching the main loop), instead of reading screamRx unlocked in
  the if-condition;
- move pthread_mutex_init(&lock_scream) ahead of pthread_create so the
  periodic thread, which now locks every iteration, never operates on an
  uninitialized mutex.

No behavioral change beyond synchronization.
In learn-from-source mode the receiver latched the sender's address from
the first RTP packet and never revised it. When the sender's public NAT
mapping moves mid-session (common on cellular/CGNAT links after tens of
minutes), feedback kept going to the stale address, so the sender stopped
receiving congestion feedback and its rate controller ran blind.

Re-latch outgoing_rtcp_addr(6) whenever the observed source ip:port
changes, gated strictly to learn-from-source mode so a seeded receiver
(explicit sender_ip) never changes its target. The periodic feedback
thread now snapshots the target under lock_scream before sendto, since
the receive thread may rewrite it concurrently.

Signed-off-by: Otto Nilsson <otto.nilsson@ericsson.com>
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