Title: [278287] trunk/Source/WebCore
Revision
278287
Author
cdu...@apple.com
Date
2021-05-31 17:07:14 -0700 (Mon, 31 May 2021)

Log Message

Fix thread safety issues in AudioBufferSourceNode
https://bugs.webkit.org/show_bug.cgi?id=226448

Reviewed by Darin Adler.

Adopt thread safety analysis annotations in AudioBufferSourceNode and fix the bugs
that were found by clang:
- We were failing to grab the processLock when setting the loop / loopStart / loopEnd attributes
  on the main thread (Those are set by JS at any time).
- propagatesSilence() was failing to grab the lock when checking m_buffer on the audio thread.

* Modules/webaudio/AudioBufferSourceNode.cpp:
(WebCore::AudioBufferSourceNode::process):
(WebCore::AudioBufferSourceNode::renderSilenceAndFinishIfNotLooping):
(WebCore::AudioBufferSourceNode::renderFromBuffer):
(WebCore::AudioBufferSourceNode::setBuffer):
(WebCore::AudioBufferSourceNode::setLoop):
(WebCore::AudioBufferSourceNode::setLoopStart):
(WebCore::AudioBufferSourceNode::setLoopEnd):
(WebCore::AudioBufferSourceNode::adjustGrainParameters):
(WebCore::AudioBufferSourceNode::totalPitchRate):
(WebCore::AudioBufferSourceNode::propagatesSilence const):
* Modules/webaudio/AudioBufferSourceNode.h:

Modified Paths

Diff

Modified: trunk/Source/WebCore/ChangeLog (278286 => 278287)


--- trunk/Source/WebCore/ChangeLog	2021-06-01 00:04:51 UTC (rev 278286)
+++ trunk/Source/WebCore/ChangeLog	2021-06-01 00:07:14 UTC (rev 278287)
@@ -1,5 +1,31 @@
 2021-05-31  Chris Dumez  <cdu...@apple.com>
 
+        Fix thread safety issues in AudioBufferSourceNode
+        https://bugs.webkit.org/show_bug.cgi?id=226448
+
+        Reviewed by Darin Adler.
+
+        Adopt thread safety analysis annotations in AudioBufferSourceNode and fix the bugs
+        that were found by clang:
+        - We were failing to grab the processLock when setting the loop / loopStart / loopEnd attributes
+          on the main thread (Those are set by JS at any time).
+        - propagatesSilence() was failing to grab the lock when checking m_buffer on the audio thread.
+
+        * Modules/webaudio/AudioBufferSourceNode.cpp:
+        (WebCore::AudioBufferSourceNode::process):
+        (WebCore::AudioBufferSourceNode::renderSilenceAndFinishIfNotLooping):
+        (WebCore::AudioBufferSourceNode::renderFromBuffer):
+        (WebCore::AudioBufferSourceNode::setBuffer):
+        (WebCore::AudioBufferSourceNode::setLoop):
+        (WebCore::AudioBufferSourceNode::setLoopStart):
+        (WebCore::AudioBufferSourceNode::setLoopEnd):
+        (WebCore::AudioBufferSourceNode::adjustGrainParameters):
+        (WebCore::AudioBufferSourceNode::totalPitchRate):
+        (WebCore::AudioBufferSourceNode::propagatesSilence const):
+        * Modules/webaudio/AudioBufferSourceNode.h:
+
+2021-05-31  Chris Dumez  <cdu...@apple.com>
+
         Stop using WTF_IGNORES_THREAD_SAFETY_ANALYSIS in MediaRecorderPrivateWriter code
         https://bugs.webkit.org/show_bug.cgi?id=226446
 

Modified: trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp (278286 => 278287)


--- trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp	2021-06-01 00:04:51 UTC (rev 278286)
+++ trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp	2021-06-01 00:07:14 UTC (rev 278287)
@@ -66,11 +66,11 @@
 {
     auto node = adoptRef(*new AudioBufferSourceNode(context));
 
-    node->setBuffer(WTFMove(options.buffer));
+    node->setBufferForBindings(WTFMove(options.buffer));
     node->detune().setValue(options.detune);
-    node->setLoop(options.loop);
-    node->setLoopEnd(options.loopEnd);
-    node->setLoopStart(options.loopStart);
+    node->setLoopForBindings(options.loop);
+    node->setLoopEndForBindings(options.loopEnd);
+    node->setLoopStartForBindings(options.loopStart);
     node->playbackRate().setValue(options.playbackRate);
 
     return node;
@@ -110,7 +110,7 @@
     }
     Locker locker { AdoptLock, m_processLock };
 
-    if (!buffer()) {
+    if (!m_buffer) {
         outputBus.zero();
         return;
     }
@@ -118,7 +118,7 @@
     // After calling setBuffer() with a buffer having a different number of channels, there can in rare cases be a slight delay
     // before the output bus is updated to the new number of channels because of use of tryLocks() in the context's updating system.
     // In this case, if the buffer has just been changed and we're not quite ready yet, then just output silence.
-    if (numberOfChannels() != buffer()->numberOfChannels()) {
+    if (numberOfChannels() != m_buffer->numberOfChannels()) {
         outputBus.zero();
         return;
     }
@@ -148,7 +148,7 @@
 // Returns true if we're finished.
 bool AudioBufferSourceNode::renderSilenceAndFinishIfNotLooping(AudioBus*, unsigned index, size_t framesToProcess)
 {
-    if (!loop()) {
+    if (!m_isLooping) {
         // If we're not looping, then stop playing when we get to the end.
 
         if (framesToProcess > 0) {
@@ -171,8 +171,8 @@
 
     // Basic sanity checking
     ASSERT(bus);
-    ASSERT(buffer());
-    if (!bus || !buffer())
+    ASSERT(m_buffer);
+    if (!bus || !m_buffer)
         return false;
 
     unsigned numberOfChannels = this->numberOfChannels();
@@ -205,8 +205,8 @@
     // Offset the pointers to the correct offset frame.
     unsigned writeIndex = destinationFrameOffset;
 
-    size_t bufferLength = buffer()->length();
-    double bufferSampleRate = buffer()->sampleRate();
+    size_t bufferLength = m_buffer->length();
+    double bufferSampleRate = m_buffer->sampleRate();
     double pitchRate = totalPitchRate();
     bool reverse = pitchRate < 0;
 
@@ -228,10 +228,10 @@
     double virtualMinFrame = 0;
     double virtualDeltaFrames = maxFrame;
 
-    if (loop() && (m_loopStart || m_loopEnd) && m_loopStart >= 0 && m_loopEnd > 0 && m_loopStart < m_loopEnd) {
+    if (m_isLooping && (m_loopStart || m_loopEnd) && m_loopStart >= 0 && m_loopEnd > 0 && m_loopStart < m_loopEnd) {
         // Convert from seconds to sample-frames.
-        double loopMinFrame = m_loopStart * buffer()->sampleRate();
-        double loopMaxFrame = m_loopEnd * buffer()->sampleRate();
+        double loopMinFrame = m_loopStart * m_buffer->sampleRate();
+        double loopMaxFrame = m_loopEnd * m_buffer->sampleRate();
 
         virtualMaxFrame = std::min(loopMaxFrame, virtualMaxFrame);
         virtualMinFrame = std::max(loopMinFrame, virtualMinFrame);
@@ -240,8 +240,8 @@
 
     // If we're looping and the offset (virtualReadIndex) is past the end of the loop, wrap back to the
     // beginning of the loop. For other cases, nothing needs to be done.
-    if (loop() && m_virtualReadIndex >= virtualMaxFrame) {
-        m_virtualReadIndex = (m_loopStart < 0) ? 0 : (m_loopStart * buffer()->sampleRate());
+    if (m_isLooping && m_virtualReadIndex >= virtualMaxFrame) {
+        m_virtualReadIndex = (m_loopStart < 0) ? 0 : (m_loopStart * m_buffer->sampleRate());
         m_virtualReadIndex = std::min(m_virtualReadIndex, static_cast<double>(bufferLength - 1));
     }
 
@@ -339,7 +339,7 @@
 
             unsigned readIndex2 = readIndex + 1;
             if (readIndex2 >= maxFrame)
-                readIndex2 = loop() ? minFrame : readIndex;
+                readIndex2 = m_isLooping ? minFrame : readIndex;
 
             // Linear interpolation.
             for (unsigned i = 0; i < numberOfChannels; ++i) {
@@ -368,7 +368,7 @@
             // For linear interpolation we need the next sample-frame too.
             unsigned readIndex2 = readIndex + 1;
             if (readIndex2 >= bufferLength) {
-                if (loop()) {
+                if (m_isLooping) {
                     // Make sure to wrap around at the end of the buffer.
                     readIndex2 = static_cast<unsigned>(virtualReadIndex + 1 - virtualDeltaFrames);
                 } else
@@ -407,19 +407,19 @@
     return true;
 }
 
-ExceptionOr<void> AudioBufferSourceNode::setBuffer(RefPtr<AudioBuffer>&& buffer)
+ExceptionOr<void> AudioBufferSourceNode::setBufferForBindings(RefPtr<AudioBuffer>&& buffer)
 {
     ASSERT(isMainThread());
     DEBUG_LOG(LOGIDENTIFIER);
 
-    if (buffer && m_wasBufferSet)
-        return Exception { InvalidStateError, "The buffer was already set"_s };
-
     // The context must be locked since changing the buffer can re-configure the number of channels that are output.
     Locker contextLocker { context().graphLock() };
     
     // This synchronizes with process().
     Locker locker { m_processLock };
+
+    if (buffer && m_wasBufferSet)
+        return Exception { InvalidStateError, "The buffer was already set"_s };
     
     if (buffer) {
         m_wasBufferSet = true;
@@ -457,6 +457,27 @@
     return startPlaying(when, grainOffset, grainDuration);
 }
 
+void AudioBufferSourceNode::setLoopForBindings(bool looping)
+{
+    ASSERT(isMainThread());
+    Locker locker { m_processLock };
+    m_isLooping = looping;
+}
+
+void AudioBufferSourceNode::setLoopStartForBindings(double loopStart)
+{
+    ASSERT(isMainThread());
+    Locker locker { m_processLock };
+    m_loopStart = loopStart;
+}
+
+void AudioBufferSourceNode::setLoopEndForBindings(double loopEnd)
+{
+    ASSERT(isMainThread());
+    Locker locker { m_processLock };
+    m_loopEnd = loopEnd;
+}
+
 ExceptionOr<void> AudioBufferSourceNode::startPlaying(double when, double grainOffset, std::optional<double> grainDuration)
 {
     ASSERT(isMainThread());
@@ -496,12 +517,11 @@
 {
     ASSERT(m_processLock.isHeld());
 
-    auto buffer = this->buffer();
-    if (!buffer)
+    if (!m_buffer)
         return;
 
     // Do sanity checking of grain parameters versus buffer size.
-    double bufferDuration = buffer->duration();
+    double bufferDuration = m_buffer->duration();
 
     m_grainOffset = std::min(bufferDuration, m_grainOffset);
 
@@ -508,7 +528,7 @@
     if (!m_wasGrainDurationGiven)
         m_grainDuration = bufferDuration - m_grainOffset;
 
-    if (m_wasGrainDurationGiven && loop()) {
+    if (m_wasGrainDurationGiven && m_isLooping) {
         // We're looping a grain with a grain duration specified. Schedule the loop
         // to stop after grainDuration seconds after starting, possibly running the
         // loop multiple times if grainDuration is larger than the buffer duration.
@@ -523,9 +543,9 @@
     // When aligned to the sample-frame the playback will be identical to the PCM data stored in the buffer.
     // Since playbackRate == 1 is very common, it's worth considering quality.
     if (playbackRate().value() < 0)
-        m_virtualReadIndex = AudioUtilities::timeToSampleFrame(m_grainOffset + m_grainDuration, buffer->sampleRate()) - 1;
+        m_virtualReadIndex = AudioUtilities::timeToSampleFrame(m_grainOffset + m_grainDuration, m_buffer->sampleRate()) - 1;
     else
-        m_virtualReadIndex = AudioUtilities::timeToSampleFrame(m_grainOffset, buffer->sampleRate());
+        m_virtualReadIndex = AudioUtilities::timeToSampleFrame(m_grainOffset, m_buffer->sampleRate());
 }
 
 double AudioBufferSourceNode::totalPitchRate()
@@ -533,8 +553,8 @@
     // Incorporate buffer's sample-rate versus AudioContext's sample-rate.
     // Normally it's not an issue because buffers are loaded at the AudioContext's sample-rate, but we can handle it in any case.
     double sampleRateFactor = 1.0;
-    if (buffer())
-        sampleRateFactor = buffer()->sampleRate() / static_cast<double>(sampleRate());
+    if (m_buffer)
+        sampleRateFactor = m_buffer->sampleRate() / static_cast<double>(sampleRate());
     
     double basePitchRate = playbackRate().finalValue();
     double detune = pow(2, m_detune->finalValue() / 1200);
@@ -553,7 +573,16 @@
 
 bool AudioBufferSourceNode::propagatesSilence() const
 {
-    return !isPlayingOrScheduled() || hasFinished() || !m_buffer;
+    ASSERT(context().isAudioThread());
+    if (!isPlayingOrScheduled() || hasFinished())
+        return true;
+
+    if (!m_processLock.tryLock()) {
+        // We weren't able to get the lock so we assume we have a buffer.
+        return false;
+    }
+    Locker locker { AdoptLock, m_processLock };
+    return !m_buffer;
 }
 
 } // namespace WebCore

Modified: trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h (278286 => 278287)


--- trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h	2021-06-01 00:04:51 UTC (rev 278286)
+++ trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.h	2021-06-01 00:07:14 UTC (rev 278287)
@@ -49,10 +49,12 @@
     // AudioNode
     void process(size_t framesToProcess) final;
 
-    // setBuffer() is called on the main thread.  This is the buffer we use for playback.
-    ExceptionOr<void> setBuffer(RefPtr<AudioBuffer>&&);
-    AudioBuffer* buffer() { return m_buffer.get(); }
+    // setBufferForBindings() is called on the main thread. This is the buffer we use for playback.
+    ExceptionOr<void> setBufferForBindings(RefPtr<AudioBuffer>&&);
 
+    // This function does not lock before accessing the buffer and should therefore only be called on the main thread.
+    AudioBuffer* bufferForBindings() WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_buffer.get(); }
+
     // numberOfChannels() returns the number of output channels.  This value equals the number of channels from the buffer.
     // If a new buffer is set with a different number of channels, then this value will dynamically change.
     unsigned numberOfChannels();
@@ -60,17 +62,15 @@
     // Play-state
     ExceptionOr<void> startLater(double when, double grainOffset, std::optional<double> grainDuration);
 
-    // Note: the attribute was originally exposed as .looping, but to be more consistent in naming with <audio>
-    // and with how it's described in the specification, the proper attribute name is .loop
-    // The old attribute is kept for backwards compatibility.
-    bool loop() const { return m_isLooping; }
-    void setLoop(bool looping) { m_isLooping = looping; }
+    // This function doesn't grab the lock before accessing m_isLooping and is thus only safe to call on the main thread.
+    bool loopForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_isLooping; }
+    void setLoopForBindings(bool);
 
     // Loop times in seconds.
-    double loopStart() const { return m_loopStart; }
-    double loopEnd() const { return m_loopEnd; }
-    void setLoopStart(double loopStart) { m_loopStart = loopStart; }
-    void setLoopEnd(double loopEnd) { m_loopEnd = loopEnd; }
+    double loopStartForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_loopStart; } // This function doesn't grab the lock and is thus only safe to call on the main thread.
+    double loopEndForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_loopEnd; } // This function doesn't grab the lock and is thus only safe to call on the main thread.
+    void setLoopStartForBindings(double);
+    void setLoopEndForBindings(double);
 
     AudioParam& detune() { return m_detune.get(); }
     AudioParam& playbackRate() { return m_playbackRate.get(); }
@@ -88,16 +88,16 @@
     double latencyTime() const final { return 0; }
 
     ExceptionOr<void> startPlaying(double when, double grainOffset, std::optional<double> grainDuration);
-    void adjustGrainParameters();
+    void adjustGrainParameters() WTF_REQUIRES_LOCK(m_processLock);
 
     // Returns true on success.
-    bool renderFromBuffer(AudioBus*, unsigned destinationFrameOffset, size_t numberOfFrames, double startFrameOffset);
+    bool renderFromBuffer(AudioBus*, unsigned destinationFrameOffset, size_t numberOfFrames, double startFrameOffset) WTF_REQUIRES_LOCK(m_processLock);
 
     // Render silence starting from "index" frame in AudioBus.
-    inline bool renderSilenceAndFinishIfNotLooping(AudioBus*, unsigned index, size_t framesToProcess);
+    inline bool renderSilenceAndFinishIfNotLooping(AudioBus*, unsigned index, size_t framesToProcess) WTF_REQUIRES_LOCK(m_processLock);
 
     // m_buffer holds the sample data which this node outputs.
-    RefPtr<AudioBuffer> m_buffer;
+    RefPtr<AudioBuffer> m_buffer WTF_GUARDED_BY_LOCK(m_processLock); // Only modified on the main thread but used on the audio thread.
 
     // Pointers for the buffer and destination.
     UniqueArray<const float*> m_sourceChannels;
@@ -108,12 +108,12 @@
 
     // If m_isLooping is false, then this node will be done playing and become inactive after it reaches the end of the sample data in the buffer.
     // If true, it will wrap around to the start of the buffer each time it reaches the end.
-    bool m_isLooping { false };
+    bool m_isLooping WTF_GUARDED_BY_LOCK(m_processLock) { false }; // Only modified on the main thread but queried on the audio thread.
 
     bool m_wasBufferSet { false };
 
-    double m_loopStart { 0 };
-    double m_loopEnd { 0 };
+    double m_loopStart WTF_GUARDED_BY_LOCK(m_processLock) { 0 }; // Only modified on the main thread but queried on the audio thread.
+    double m_loopEnd WTF_GUARDED_BY_LOCK(m_processLock) { 0 }; // Only modified on the main thread but queried on the audio thread.
 
     // m_virtualReadIndex is a sample-frame index into our buffer representing the current playback position.
     // Since it's floating-point, it has sub-sample accuracy.
@@ -120,14 +120,14 @@
     double m_virtualReadIndex { 0 };
 
     // Granular playback
-    bool m_isGrain { false };
-    double m_grainOffset { 0 }; // in seconds
-    double m_grainDuration; // in seconds
-    double m_wasGrainDurationGiven { false };
+    bool m_isGrain WTF_GUARDED_BY_LOCK(m_processLock) { false };
+    double m_grainOffset WTF_GUARDED_BY_LOCK(m_processLock) { 0 }; // in seconds
+    double m_grainDuration WTF_GUARDED_BY_LOCK(m_processLock); // in seconds
+    double m_wasGrainDurationGiven WTF_GUARDED_BY_LOCK(m_processLock) { false };
 
     // totalPitchRate() returns the instantaneous pitch rate (non-time preserving).
     // It incorporates the base pitch rate, any sample-rate conversion factor from the buffer.
-    double totalPitchRate();
+    double totalPitchRate() WTF_REQUIRES_LOCK(m_processLock);
 
     // This synchronizes process() with setBuffer() which can cause dynamic channel count changes.
     mutable Lock m_processLock;

Modified: trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.idl (278286 => 278287)


--- trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.idl	2021-06-01 00:04:51 UTC (rev 278286)
+++ trunk/Source/WebCore/Modules/webaudio/AudioBufferSourceNode.idl	2021-06-01 00:07:14 UTC (rev 278287)
@@ -31,14 +31,14 @@
 ] interface AudioBufferSourceNode : AudioScheduledSourceNode {
     [EnabledBySetting=WebAudio] constructor (BaseAudioContext context, optional AudioBufferSourceOptions options);
 
-    attribute AudioBuffer? buffer;
+    [ImplementedAs=bufferForBindings] attribute AudioBuffer? buffer;
 
     readonly attribute AudioParam playbackRate;
     readonly attribute AudioParam detune;
 
-    attribute boolean loop;
-    attribute double loopStart;
-    attribute double loopEnd;
+    [ImplementedAs=loopForBindings] attribute boolean loop;
+    [ImplementedAs=loopStartForBindings] attribute double loopStart;
+    [ImplementedAs=loopEndForBindings] attribute double loopEnd;
 
     [ImplementedAs=startLater] undefined start(optional double when = 0, optional double grainOffset = 0, optional double grainDuration);
 };
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to