← WebKit — 2026-W37

42ca5cc802a1492a3d701874b0e7cbd03aa2f948  MockRealtimeVideoSource shares five members between its capture run loop and its caller's thread without synchronization

severity low class Race confidence 0.4 signal score 8.15 Source/WebCore/platform
Primitive: data races on MockRealtimeVideoSource m_preset/m_startTime/m_elapsedTime/m_deviceOrientation between caller and capture run-loop threads
Triage note: Renames m_imageBufferLock->m_frameGenerationLock and extends it to guard m_preset/m_startTime/m_elapsedTime, makes m_deviceOrientation atomic, and dispatches delaySamples onto m_runLoop. Genuine thread-safety fix but in mock-capture infrastructure, lowering real-world severity.
Structural signals: smartptr_member_added (+3.0), null_guard (+1.95), uaf_lifetime (+2.2), msg_soft (+1.0)

Commit message

MockRealtimeVideoSource shares five members between its capture run loop and its caller's thread without synchronization
https://bugs.webkit.org/show_bug.cgi?id=323742

Reviewed by Jean-Yves Avenard.

Frames are generated on m_runLoop, a dedicated run loop, while the source is controlled from
the caller's thread; generateFrame() and generatePhoto() assert !isMainThread() to say so. The
class already guards m_imageBuffer and m_drawingState with a lock, makes m_isTakingPhoto and
m_captureWasInterrupted atomic, and marshals timer start and stop onto m_runLoop. Five members
were left out of all of that, and the run loop reaches four of them indirectly, through helper
calls, which is what kept them hidden:

- m_startTime and m_elapsedTime are written by startProducingData() and stopProducingData(),
  and read by elapsedTime(), which drawText() and MockRealtimeVideoSourceMac::updateSampleBuffer()
  both call while generating a frame.
- m_preset is written by applyFrameRateAndZoomWithPreset() and read by captureSize(), which the
  whole draw path calls.
- m_deviceOrientation is written by orientationChanged() and
  rotationAngleForHorizonLevelDisplayChanged(), and read by videoFrameRotation() from
  updateSampleBuffer().
- m_delayUntil is written by delaySamples() and both read and cleared by generateFrame().

Each is fixed by whichever mechanism matches how it is reached rather than by one blanket
approach. m_startTime, m_elapsedTime and m_preset become WTF_GUARDED_BY_LOCK: every reader on
the run loop already held the lock, so only the writers needed changing, and elapsedTime() and
captureSize() get assertIsHeld() to match how the surrounding code already declares this.
m_deviceOrientation becomes atomic instead, because the lock does not fit it: videoFrameRotation()
is virtual on RealtimeMediaSource and so may be called from anywhere, and settings() reads the
member on the caller's thread. m_delayUntil moves entirely onto m_runLoop by dispatching from
delaySamples(), the same way startCaptureTimer() and stopCaptureTimer() already work; the
deadline is still computed at call time so it does not shift with dispatch latency.

Annotating m_preset immediately turned up a third accessor that reading the code had not:
settings() reads it on the caller's thread, which now takes the lock for that one read.
applyFrameRateAndZoomWithPreset() is restructured so setIntrinsicSize(), which notifies
observers, is not called while holding the lock.

The lock is renamed from m_imageBufferLock to m_frameGenerationLock. It already guarded
m_drawingState and now covers three further members that are not image buffers.

None of this is reachable by web content: MockRealtimeVideoSource is only created by
MockRealtimeMediaSourceCenter, that is, when mock capture devices are enabled for testing. Nor
was any of it a memory-safety problem, since everything read across the thread boundary is
plain data, a MonotonicTime, a Seconds, an enum, or the IntSize inside m_preset. The visible
symptom would have been a wrong timestamp, rotation or frame size in a generated mock frame.

* Source/WebCore/platform/mock/MockRealtimeVideoSource.cpp:
(WebCore::MockRealtimeVideoSource::takePhotoInternal):
(WebCore::MockRealtimeVideoSource::settings):
(WebCore::MockRealtimeVideoSource::applyFrameRateAndZoomWithPreset):
(WebCore::MockRealtimeVideoSource::captureSize const):
(WebCore::MockRealtimeVideoSource::invalidateDrawingState):
(WebCore::MockRealtimeVideoSource::drawingState):
(WebCore::MockRealtimeVideoSource::settingsDidChange):
(WebCore::MockRealtimeVideoSource::startProducingData):
(WebCore::MockRealtimeVideoSource::stopProducingData):
(WebCore::MockRealtimeVideoSource::elapsedTime):
(WebCore::MockRealtimeVideoSource::drawText):
(WebCore::MockRealtimeVideoSource::delaySamples):
(WebCore::MockRealtimeVideoSource::generatePhoto):
(WebCore::MockRealtimeVideoSource::generateFrameInternal):
(WebCore::MockRealtimeVideoSource::generateFrame):
(WebCore::MockRealtimeVideoSource::imageBuffer):
(WebCore::MockRealtimeVideoSource::imageBufferInternal):
(WebCore::MockRealtimeVideoSource::orientationChanged):
* Source/WebCore/platform/mock/MockRealtimeVideoSource.h:
(WebCore::MockRealtimeVideoSource::WTF_GUARDED_BY_LOCK):

Canonical link: https://commits.webkit.org/320813@main

Before / after

Loading diff…