Bug 1253590, part 1 - Generalize AutoReferenceLoopDetector to allow it to be used to limit reference chain lengths. r=longsonr
authorJonathan Watt <jwatt@jwatt.org>
Wed, 09 Mar 2016 10:26:48 +0000
changeset 290197 1ec19ec66b5dbf8224a0f3a5dbf1ca890d64cf6f
parent 290196 b057d3cb5c1b37dac799e6eabe9384a4f6db7eee
child 290198 d6e3295ba91596dece2b32f76526433a16ad39cd
push id30114
push usercbook@mozilla.com
push dateThu, 24 Mar 2016 15:15:54 +0000
treeherdermozilla-central@24c5fbde4488 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslongsonr
bugs1253590
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1253590, part 1 - Generalize AutoReferenceLoopDetector to allow it to be used to limit reference chain lengths. r=longsonr
layout/svg/AutoReferenceLimiter.h
layout/svg/nsSVGClipPathFrame.cpp
layout/svg/nsSVGClipPathFrame.h
new file mode 100644
--- /dev/null
+++ b/layout/svg/AutoReferenceLimiter.h
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NS_AUTOREFERENCELIMITER_H
+#define NS_AUTOREFERENCELIMITER_H
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ReentrancyGuard.h"
+#include "nsDebug.h"
+
+namespace mozilla {
+
+/**
+ * This helper allows us to handle two related issues in SVG content: reference
+ * loops, and reference chains that we deem to be too long.
+ *
+ * SVG content may contain reference loops where an SVG effect (a clipPath,
+ * say) may reference itself either directly or, perhaps more likely,
+ * indirectly via a reference chain to other elements that eventually leads
+ * back to itself.  This helper class allows us to detect and immediately break
+ * such reference loops when applying an effect so that we can prevent
+ * reference loops causing us to recurse until we run out of stack space and
+ * crash.
+ *
+ * SVG also allows for (non-loop) reference chains of arbitrary length, the
+ * length depending entirely on the SVG content.  Some SVG authoring tools have
+ * been known to create absurdly long reference chains.  (For example, bug
+ * 1253590 details a case where Adobe Illustrator created an SVG with a chain
+ * of 5000 clip paths which could cause us to run out of stack space and
+ * crash.)  This helper class also allows us to limit the number of times we
+ * recurse into a function, thereby allowing us to limit the length ofreference
+ * chains.
+ *
+ * Consumers that need to handle the reference loop case should add a member
+ * variable (mReferencing, say) to the class that represents and applies the
+ * SVG effect in question (typically an nsIFrame sub-class), initialize that
+ * member to AutoReferenceLimiter::notReferencing in the class' constructor
+ * (and never touch that variable again), and then add something like the
+ * following at the top of the method(s) that may recurse to follow references
+ * when applying an effect:
+ *
+ *   AutoReferenceLimiter refLoopDetector(&mInUse, 1); // only one ref allowed
+ *   if (!refLoopDetector.Reference()) {
+ *     return; // reference loop
+ *   }
+ *
+ * Consumers that need to limit reference chain lengths should add something
+ * like the following code at the top of the method(s) that may recurse to
+ * follow references when applying an effect:
+ *
+ *   static int16_t sChainLengthCounter = AutoReferenceLimiter::notReferencing;
+ *
+ *   AutoReferenceLimiter refChainLengthLimiter(&sChainLengthCounter, MAX_LEN);
+ *   if (!refChainLengthLimiter.Reference()) {
+ *     return; // reference chain too long
+ *   }
+ */
+class MOZ_RAII AutoReferenceLimiter
+{
+public:
+  static const int16_t notReferencing = -2;
+
+  AutoReferenceLimiter(int16_t* aRefCounter, int16_t aMaxReferenceCount)
+  {
+    MOZ_ASSERT(aMaxReferenceCount > 0 &&
+               aRefCounter &&
+               (*aRefCounter == notReferencing ||
+                (*aRefCounter >= 0 && *aRefCounter < aMaxReferenceCount)));
+
+    if (*aRefCounter == notReferencing) {
+      // initialize
+      *aRefCounter = aMaxReferenceCount;
+    }
+    mRefCounter = aRefCounter;
+    mMaxReferenceCount = aMaxReferenceCount;
+  }
+
+  ~AutoReferenceLimiter() {
+    // If we fail this assert then there were more destructor calls than
+    // Reference() calls (a consumer forgot to to call Reference()), or else
+    // someone messed with the variable pointed to by mRefCounter.
+    MOZ_ASSERT(*mRefCounter < mMaxReferenceCount);
+
+    (*mRefCounter)++;
+
+    if (*mRefCounter == mMaxReferenceCount) {
+      *mRefCounter = notReferencing; // reset ready for use next time
+    }
+  }
+
+  /**
+   * Returns true on success (no reference loop/reference chain length is
+   * within the specified limits), else returns false on failure (there is a
+   * reference loop/the reference chain has exceeded the specified limits).
+   */
+  MOZ_WARN_UNUSED_RESULT bool Reference() {
+    // If we fail this assertion then either a consumer failed to break a
+    // reference loop/chain, or else they called Reference() more than once
+    MOZ_ASSERT(*mRefCounter >= 0);
+
+    (*mRefCounter)--;
+
+    if (*mRefCounter < 0) {
+      // TODO: This is an issue with the document, not with Mozilla code. We
+      // should stop using NS_WARNING and send a message to the console
+      // instead (but only once per document, not over and over as we repaint).
+      if (mMaxReferenceCount == 1) {
+        NS_WARNING("Reference loop detected!");
+      } else {
+        NS_WARNING("Reference chain length limit exceeded!");
+      }
+      return false;
+    }
+    return true;
+  }
+
+private:
+  int16_t* mRefCounter;
+  int16_t mMaxReferenceCount;
+};
+
+} // namespace mozilla
+
+#endif // NS_AUTOREFERENCELIMITER_H
--- a/layout/svg/nsSVGClipPathFrame.cpp
+++ b/layout/svg/nsSVGClipPathFrame.cpp
@@ -34,17 +34,17 @@ void
 nsSVGClipPathFrame::ApplyClipPath(gfxContext& aContext,
                                   nsIFrame* aClippedFrame,
                                   const gfxMatrix& aMatrix)
 {
   MOZ_ASSERT(IsTrivial(), "Caller needs to use GetClipMask");
 
   DrawTarget& aDrawTarget = *aContext.GetDrawTarget();
 
-  // No need for AutoReferenceLoopDetector since simple clip paths can't create
+  // No need for AutoReferenceLimiter since simple clip paths can't create
   // a reference loop (they don't reference other clip paths).
 
   // Restore current transform after applying clip path:
   gfxContextMatrixAutoSaveRestore autoRestore(&aContext);
 
   RefPtr<Path> clipPath;
 
   nsISVGChildFrame* singleClipPathChild = nullptr;
@@ -85,20 +85,21 @@ nsSVGClipPathFrame::GetClipMask(gfxConte
                                 Matrix* aMaskTransform,
                                 SourceSurface* aExtraMask,
                                 const Matrix& aExtraMasksTransform)
 {
   MOZ_ASSERT(!IsTrivial(), "Caller needs to use ApplyClipPath");
 
   DrawTarget& aReferenceDT = *aReferenceContext.GetDrawTarget();
 
-  AutoReferenceLoopDetector loopDetector;
-  if (!loopDetector.MarkAsInUse(this)) {
-    // Reference loop! This reference should be ignored, so return nullptr.
-    return nullptr;
+  // To prevent reference loops we check that this clipPath only appears
+  // once in the reference chain (if any) that we're currently processing:
+  AutoReferenceLimiter refLoopDetector(&mReferencing, 1);
+  if (!refLoopDetector.Reference()) {
+    return nullptr; // Reference loop!
   }
 
   IntRect devSpaceClipExtents;
   {
     gfxContextMatrixAutoSaveRestore autoRestoreMatrix(&aReferenceContext);
 
     aReferenceContext.SetMatrix(gfxMatrix());
     gfxRect rect = aReferenceContext.GetClipExtents();
@@ -233,21 +234,21 @@ nsSVGClipPathFrame::GetClipMask(gfxConte
   *aMaskTransform = ToMatrix(mat);
   return maskDT->Snapshot();
 }
 
 bool
 nsSVGClipPathFrame::PointIsInsideClipPath(nsIFrame* aClippedFrame,
                                           const gfxPoint &aPoint)
 {
-  AutoReferenceLoopDetector loopDetector;
-  if (!loopDetector.MarkAsInUse(this)) {
-    // Reference loop! This reference is ignored, so return true (point not
-    // clipped out).
-    return true;
+  // To prevent reference loops we check that this clipPath only appears
+  // once in the reference chain (if any) that we're currently processing:
+  AutoReferenceLimiter refLoopDetector(&mReferencing, 1);
+  if (!refLoopDetector.Reference()) {
+    return true; // Reference loop!
   }
 
   gfxMatrix matrix = GetClipPathTransform(aClippedFrame);
   if (!matrix.Invert()) {
     return false;
   }
   gfxPoint point = matrix.Transform(aPoint);
 
@@ -317,18 +318,20 @@ nsSVGClipPathFrame::IsTrivial(nsISVGChil
     *aSingleChild = foundChild;
   }
   return true;
 }
 
 bool
 nsSVGClipPathFrame::IsValid()
 {
-  AutoReferenceLoopDetector loopDetector;
-  if (!loopDetector.MarkAsInUse(this)) {
+  // To prevent reference loops we check that this clipPath only appears
+  // once in the reference chain (if any) that we're currently processing:
+  AutoReferenceLimiter refLoopDetector(&mReferencing, 1);
+  if (!refLoopDetector.Reference()) {
     return false; // Reference loop!
   }
 
   bool isOK = true;
   nsSVGEffects::GetEffectProperties(this).GetClipPathFrame(&isOK);
   if (!isOK) {
     return false;
   }
--- a/layout/svg/nsSVGClipPathFrame.h
+++ b/layout/svg/nsSVGClipPathFrame.h
@@ -1,18 +1,19 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef __NS_SVGCLIPPATHFRAME_H__
 #define __NS_SVGCLIPPATHFRAME_H__
 
+#include "AutoReferenceLimiter.h"
+#include "gfxMatrix.h"
 #include "mozilla/Attributes.h"
-#include "gfxMatrix.h"
 #include "nsSVGContainerFrame.h"
 #include "nsSVGUtils.h"
 
 class gfxContext;
 class nsISVGChildFrame;
 
 typedef nsSVGContainerFrame nsSVGClipPathFrameBase;
 
@@ -22,17 +23,17 @@ class nsSVGClipPathFrame : public nsSVGC
   NS_NewSVGClipPathFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 
   typedef mozilla::gfx::Matrix Matrix;
   typedef mozilla::gfx::SourceSurface SourceSurface;
 
 protected:
   explicit nsSVGClipPathFrame(nsStyleContext* aContext)
     : nsSVGClipPathFrameBase(aContext)
-    , mInUse(false)
+    , mReferencing(mozilla::AutoReferenceLimiter::notReferencing)
   {
     AddStateBits(NS_FRAME_IS_NONDISPLAY);
   }
 
 public:
   NS_DECL_FRAMEARENA_HELPERS
 
   // nsIFrame methods:
@@ -130,82 +131,29 @@ public:
    */
   gfxMatrix GetClipPathTransform(nsIFrame* aClippedFrame);
 
 private:
 
   // nsSVGContainerFrame methods:
   virtual gfxMatrix GetCanvasTM() override;
 
-  /**
-   * SVG content may contain reference loops where an SVG effect (a clipPath,
-   * say) may reference itself (directly or indirectly via a reference chain).
-   * This helper class allows us to detect and break such reference loops when
-   * applying an effect so that we can safely do so without the reference loop
-   * causing us to recurse until we run out of stack space and crash.
-   * The helper automatically sets and clears the mInUse flag on the frame.
-   */
-  class MOZ_RAII AutoReferenceLoopDetector
-  {
-  public:
-    explicit AutoReferenceLoopDetector()
-       : mFrame(nullptr)
-#ifdef DEBUG
-       , mMarkAsInUseCalled(false)
-#endif
-    {}
-
-    ~AutoReferenceLoopDetector() {
-      MOZ_ASSERT(mMarkAsInUseCalled,
-                 "Instances of this class are useless if MarkAsInUse() is "
-                 "not called on them");
-      if (mFrame) {
-        mFrame->mInUse = false;
-      }
-    }
-
-    /**
-     * Returns true on success (no reference loop), else returns false on
-     * failure (aFrame is already in use; that is, there is a reference loop).
-     */
-    MOZ_WARN_UNUSED_RESULT bool MarkAsInUse(nsSVGClipPathFrame* aFrame) {
-#ifdef DEBUG
-      MOZ_ASSERT(!mMarkAsInUseCalled, "Must only be called once");
-      mMarkAsInUseCalled = true;
-#endif
-      if (aFrame->mInUse) {
-        // XXX This is an error in the document, not in Mozilla code, so stop
-        // using NS_WARNING and send a message to the console instead.
-        NS_WARNING("clipPath reference loop!");
-        return false;
-      }
-      aFrame->mInUse = true;
-      mFrame = aFrame;
-      return true;
-    }
-
-  private:
-    nsSVGClipPathFrame* mFrame;
-#ifdef DEBUG
-    bool mMarkAsInUseCalled;
-#endif
-  };
-
   // Set, during a GetClipMask() call, to the transform that still needs to be
   // concatenated to the transform of the DrawTarget that was passed to
   // GetClipMask in order to establish the coordinate space that the clipPath
   // establishes for its contents (i.e. including applying 'clipPathUnits' and
   // any 'transform' attribute set on the clipPath) specifically for clipping
   // the frame that was passed to GetClipMask at that moment in time.  This is
   // set so that if our GetCanvasTM method is called while GetClipMask is
   // painting its children, the returned matrix will include the transforms
   // that should be used when creating the mask for the frame passed to
   // GetClipMask.
   //
   // Note: The removal of GetCanvasTM is nearly complete, so our GetCanvasTM
   // may not even be called soon/any more.
   gfxMatrix mMatrixForChildren;
 
-  // Flag used by AutoReferenceLoopDetector to protect against reference loops:
-  bool mInUse;
+  // Flag used by AutoReferenceLimiter while we're processing an instance of
+  // this class to protect against (break) reference loops.
+  int16_t mReferencing;
 };
 
 #endif