Serialize recreateEncoder with encode goroutine#157
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #157 +/- ##
==========================================
+ Coverage 45.43% 45.52% +0.09%
==========================================
Files 18 18
Lines 1774 1777 +3
==========================================
+ Hits 806 809 +3
Misses 901 901
Partials 67 67
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
hanguyen-nuro
approved these changes
Jun 3, 2026
Per-track encoderMu (sync.RWMutex). The encode goroutine holds RLock from encodedReader.Read through release(); recreateEncoder takes Lock to swap the reader without racing the goroutine. Before this, SetBitrateAllocation calling recreateEncoder on a 0->positive transition could close the old encoder, reset the shared FrameBuffer, or create a second NewEncodedReader while the per-track encode goroutine was inside Read() or holding a release() callback on the old encoder. Lock order: tracksMu (write) then encoderMu (write); readers take them sequentially and never nest.
fdd41e0 to
5d33db7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
After #156 (event-driven encode),
SetBitrateAllocationcallingrecreateEncoderon a 0→positive transition can race the per-track encode goroutine:track.encodedReaderundertracksMu.RLock, releases the lock, then callsRead()on the snapshotted pointer.recreateEncoderruns undertracksMu.Lock()and does:fb.ResetInitialized()(temporarily flips FrameBuffer's Read semantics)mediaTrack.NewEncodedReader(...)(creates a second encoder consuming from the same FrameBuffer)track.encodedReader.Close()(destroys the old encoder)track.encodedReaderRace vectors:
release()afterClose(): the goroutine's in-flightRead()returns(encoded, release, err). By the time it invokesrelease(), the old encoder may have been destroyed. Whether that's a no-op, double-free, or UAF depends on pion/mediadevices internals; not a documented contract.NewEncodedReader's codec-detection reads, the old encoder is still consuming from the same FrameBuffer. Real frames can be stolen between the two encoders.fbis uninitialized, the old encoder's pendingReadfalls through the uninitialized path and can emit a black frame as if real.vpx.encoder.muserializesReadvsCloseat the vpx layer, so the bare crash window insideClose-mid-Read is narrow — but therelease()-after-Close()window is not protected by it.Fix
Per-track
encoderMu sync.RWMutex.encoderMu.RLock()immediately beforeencodedReader.Read()and holds it throughrelease(). The lock is released regardless of success/error viadefer.recreateEncodertakesencoderMu.Lock()for the duration of the swap (FrameBuffer reset,NewEncodedReader,Close, pointer assignment).Lock ordering (consistent, no deadlock):
tracksMu.LockthenencoderMu.Lock.tracksMu.RLockfor a brief snapshot, thentracksMu.RUnlock, thenencoderMu.RLock— locks are never nested.Cost:
encoderMu.Lockblocks until the in-flight encode cycle (Read + WriteSample + release) completes — tens of ms typically.recreateEncoderis infrequent (0→positive bitrate transitions only), so the latency cost is acceptable.Tests
go test -race -count=1 ./sender/...— clean.go test -race -bench BenchmarkUpdateBitrate -benchtime=500ms— clean, no regression vs main.Reported by @jcui on a downstream consumer that calls
SetBitrateAllocationimmediately afterAddVideoTrackduring init, triggering 0→positive transitions on every track while encode goroutines are already running.