Bug 1330593 part 1 - Allow non-flat external strings. r=jwalden, r=bz, a=jcristau
authorJan de Mooij <jdemooij@mozilla.com>
Wed, 01 Feb 2017 18:49:47 -0500
changeset 366841 93b639dcd0c2c6231932bdd8279dc858f6596bf6
parent 366840 ec8f0f613a8c0c0470bfea9788a39e7fea9a53d5
child 366842 a243cd4ce4480144c8c605f57ca54e0aaa1dcf7a
push id6864
push userryanvm@gmail.com
push dateThu, 02 Feb 2017 00:08:19 +0000
treeherdermozilla-beta@93b639dcd0c2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalden, bz, jcristau
bugs1330593
milestone52.0
Bug 1330593 part 1 - Allow non-flat external strings. r=jwalden, r=bz, a=jcristau
js/public/Class.h
js/src/builtin/TestingFunctions.cpp
js/src/jit-test/tests/basic/external-strings.js
js/src/jit/VMFunctions.cpp
js/src/jsapi-tests/testExternalStrings.cpp
js/src/vm/String-inl.h
js/src/vm/String.cpp
js/src/vm/String.h
js/xpconnect/src/XPCString.cpp
js/xpconnect/src/xpcpublic.h
--- a/js/public/Class.h
+++ b/js/public/Class.h
@@ -359,17 +359,17 @@ typedef bool
  * from other live objects or from GC roots.  Obviously, finalizers must never
  * store a reference to obj.
  */
 typedef void
 (* JSFinalizeOp)(JSFreeOp* fop, JSObject* obj);
 
 /** Finalizes external strings created by JS_NewExternalString. */
 struct JSStringFinalizer {
-    void (*finalize)(const JSStringFinalizer* fin, char16_t* chars);
+    void (*finalize)(JS::Zone* zone, const JSStringFinalizer* fin, char16_t* chars);
 };
 
 /**
  * Check whether v is an instance of obj.  Return false on error or exception,
  * true on success with true in *bp if v is an instance of obj, false in
  * *bp otherwise.
  */
 typedef bool
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -1235,16 +1235,73 @@ EnableTrackAllocations(JSContext* cx, un
 
 static bool
 DisableTrackAllocations(JSContext* cx, unsigned argc, Value* vp)
 {
     SetAllocationMetadataBuilder(cx, nullptr);
     return true;
 }
 
+static void
+FinalizeExternalString(Zone* zone, const JSStringFinalizer* fin, char16_t* chars);
+
+static const JSStringFinalizer ExternalStringFinalizer =
+    { FinalizeExternalString };
+
+static void
+FinalizeExternalString(Zone* zone, const JSStringFinalizer* fin, char16_t* chars)
+{
+    MOZ_ASSERT(fin == &ExternalStringFinalizer);
+    js_free(chars);
+}
+
+static bool
+NewExternalString(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (args.length() != 1 || !args[0].isString()) {
+        JS_ReportErrorASCII(cx, "newExternalString takes exactly one string argument.");
+        return false;
+    }
+
+    RootedString str(cx, args[0].toString());
+    size_t len = str->length();
+
+    UniqueTwoByteChars buf(js_pod_malloc<char16_t>(len));
+    if (!JS_CopyStringChars(cx, mozilla::Range<char16_t>(buf.get(), len), str))
+        return false;
+
+    JSString* res = JS_NewExternalString(cx, buf.get(), len, &ExternalStringFinalizer);
+    if (!res)
+        return false;
+
+    mozilla::Unused << buf.release();
+    args.rval().setString(res);
+    return true;
+}
+
+static bool
+EnsureFlatString(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+
+    if (args.length() != 1 || !args[0].isString()) {
+        JS_ReportErrorASCII(cx, "ensureFlatString takes exactly one string argument.");
+        return false;
+    }
+
+    JSFlatString* flat = args[0].toString()->ensureFlat(cx);
+    if (!flat)
+        return false;
+
+    args.rval().setString(flat);
+    return true;
+}
+
 #if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
 static bool
 OOMThreadTypes(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     args.rval().setInt32(js::oom::THREAD_TYPE_MAX);
     return true;
 }
@@ -4220,16 +4277,24 @@ static const JSFunctionSpecWithHelp Test
 "  Start capturing the JS stack at every allocation. Note that this sets an\n"
 "  object metadata callback that will override any other object metadata\n"
 "  callback that may be set."),
 
     JS_FN_HELP("disableTrackAllocations", DisableTrackAllocations, 0, 0,
 "disableTrackAllocations()",
 "  Stop capturing the JS stack at every allocation."),
 
+    JS_FN_HELP("newExternalString", NewExternalString, 1, 0,
+"newExternalString(str)",
+"  Copies str's chars and returns a new external string."),
+
+    JS_FN_HELP("ensureFlatString", EnsureFlatString, 1, 0,
+"ensureFlatString(str)",
+"  Ensures str is a flat (null-terminated) string and returns it."),
+
 #if defined(DEBUG) || defined(JS_OOM_BREAKPOINT)
     JS_FN_HELP("oomThreadTypes", OOMThreadTypes, 0, 0,
 "oomThreadTypes()",
 "  Get the number of thread types that can be used as an argument for\n"
 "oomAfterAllocations() and oomAtAllocation()."),
 
     JS_FN_HELP("oomAfterAllocations", OOMAfterAllocations, 2, 0,
 "oomAfterAllocations(count [,threadType])",
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/external-strings.js
@@ -0,0 +1,16 @@
+assertEq(newExternalString(""), "");
+assertEq(newExternalString("abc"), "abc");
+assertEq(newExternalString("abc\0def\u1234"), "abc\0def\u1234");
+
+var o = {foo: 2, "foo\0": 4};
+var ext = newExternalString("foo");
+assertEq(o[ext], 2);
+var ext2 = newExternalString("foo\0");
+assertEq(o[ext2], 4);
+
+eval(newExternalString("assertEq(1, 1)"));
+
+// Make sure ensureFlat does the right thing for external strings.
+ext = newExternalString("abc\0defg\0");
+assertEq(ensureFlatString(ext), "abc\0defg\0");
+assertEq(ensureFlatString(ext), "abc\0defg\0");
--- a/js/src/jit/VMFunctions.cpp
+++ b/js/src/jit/VMFunctions.cpp
@@ -1224,17 +1224,19 @@ AssertValidStringPtr(JSContext* cx, JSSt
     if (str->isFatInline()) {
         MOZ_ASSERT(kind == gc::AllocKind::FAT_INLINE_STRING ||
                    kind == gc::AllocKind::FAT_INLINE_ATOM);
     } else if (str->isExternal()) {
         MOZ_ASSERT(kind == gc::AllocKind::EXTERNAL_STRING);
     } else if (str->isAtom()) {
         MOZ_ASSERT(kind == gc::AllocKind::ATOM);
     } else if (str->isFlat()) {
-        MOZ_ASSERT(kind == gc::AllocKind::STRING || kind == gc::AllocKind::FAT_INLINE_STRING);
+        MOZ_ASSERT(kind == gc::AllocKind::STRING ||
+                   kind == gc::AllocKind::FAT_INLINE_STRING ||
+                   kind == gc::AllocKind::EXTERNAL_STRING);
     } else {
         MOZ_ASSERT(kind == gc::AllocKind::STRING);
     }
 #endif
 }
 
 void
 AssertValidSymbolPtr(JSContext* cx, JS::Symbol* sym)
--- a/js/src/jsapi-tests/testExternalStrings.cpp
+++ b/js/src/jsapi-tests/testExternalStrings.cpp
@@ -14,23 +14,23 @@ static const char16_t arr[] = {
     'h', 'i', ',', 'd', 'o', 'n', '\'', 't', ' ', 'd', 'e', 'l', 'e', 't', 'e', ' ', 'm', 'e', '\0'
 };
 static const size_t arrlen = ArrayLength(arr) - 1;
 
 static int finalized1 = 0;
 static int finalized2 = 0;
 
 static void
-finalize_str(const JSStringFinalizer* fin, char16_t* chars);
+finalize_str(JS::Zone* zone, const JSStringFinalizer* fin, char16_t* chars);
 
 static const JSStringFinalizer finalizer1 = { finalize_str };
 static const JSStringFinalizer finalizer2 = { finalize_str };
 
 static void
-finalize_str(const JSStringFinalizer* fin, char16_t* chars)
+finalize_str(JS::Zone* zone, const JSStringFinalizer* fin, char16_t* chars)
 {
     if (chars && PodEqual(const_cast<const char16_t*>(chars), arr, arrlen)) {
         if (fin == &finalizer1) {
             ++finalized1;
         } else if (fin == &finalizer2) {
             ++finalized2;
         }
     }
--- a/js/src/vm/String-inl.h
+++ b/js/src/vm/String-inl.h
@@ -318,18 +318,16 @@ JSExternalString::init(const char16_t* c
     d.s.u2.nonInlineCharsTwoByte = chars;
     d.s.u3.externalFinalizer = fin;
 }
 
 MOZ_ALWAYS_INLINE JSExternalString*
 JSExternalString::new_(JSContext* cx, const char16_t* chars, size_t length,
                        const JSStringFinalizer* fin)
 {
-    MOZ_ASSERT(chars[length] == 0);
-
     if (!validateLength(cx, length))
         return nullptr;
     JSExternalString* str = js::Allocate<JSExternalString>(cx);
     if (!str)
         return nullptr;
     str->init(chars, length, fin);
     cx->updateMallocCounter((length + 1) * sizeof(char16_t));
     return str;
@@ -399,13 +397,21 @@ JSAtom::finalize(js::FreeOp* fop)
 
     if (!isInline())
         fop->free_(nonInlineCharsRaw());
 }
 
 inline void
 JSExternalString::finalize(js::FreeOp* fop)
 {
+    if (!JSString::isExternal()) {
+        // This started out as an external string, but was turned into a
+        // non-external string by JSExternalString::ensureFlat.
+        MOZ_ASSERT(isFlat());
+        fop->free_(nonInlineCharsRaw());
+        return;
+    }
+
     const JSStringFinalizer* fin = externalFinalizer();
-    fin->finalize(fin, const_cast<char16_t*>(rawTwoByteChars()));
+    fin->finalize(zone(), fin, const_cast<char16_t*>(rawTwoByteChars()));
 }
 
 #endif /* vm_String_inl_h */
--- a/js/src/vm/String.cpp
+++ b/js/src/vm/String.cpp
@@ -38,30 +38,30 @@ JSString::sizeOfExcludingThis(mozilla::M
         return 0;
 
     MOZ_ASSERT(isLinear());
 
     // JSDependentString: do nothing, we'll count the chars when we hit the base string.
     if (isDependent())
         return 0;
 
+    // JSExternalString: don't count, the chars could be stored anywhere.
+    if (isExternal())
+        return 0;
+
     MOZ_ASSERT(isFlat());
 
     // JSExtensibleString: count the full capacity, not just the used space.
     if (isExtensible()) {
         JSExtensibleString& extensible = asExtensible();
         return extensible.hasLatin1Chars()
                ? mallocSizeOf(extensible.rawLatin1Chars())
                : mallocSizeOf(extensible.rawTwoByteChars());
     }
 
-    // JSExternalString: don't count, the chars could be stored anywhere.
-    if (isExternal())
-        return 0;
-
     // JSInlineString, JSFatInlineString [JSInlineAtom, JSFatInlineAtom]: the chars are inline.
     if (isInline())
         return 0;
 
     // JSAtom, JSUndependedString: measure the space for the chars.  For
     // JSUndependedString, there is no need to count the base string, for the
     // same reason as JSDependentString above.
     JSFlatString& flat = asFlat();
@@ -663,17 +663,17 @@ js::ConcatStrings(ExclusiveContext* cx,
 template JSString*
 js::ConcatStrings<CanGC>(ExclusiveContext* cx, HandleString left, HandleString right);
 
 template JSString*
 js::ConcatStrings<NoGC>(ExclusiveContext* cx, JSString* const& left, JSString* const& right);
 
 template <typename CharT>
 JSFlatString*
-JSDependentString::undependInternal(ExclusiveContext* cx)
+JSDependentString::undependInternal(JSContext* cx)
 {
     size_t n = length();
     CharT* s = cx->pod_malloc<CharT>(n + 1);
     if (!s)
         return nullptr;
 
     AutoCheckCannotGC nogc;
     PodCopy(s, nonInlineChars<CharT>(nogc), n);
@@ -688,17 +688,17 @@ JSDependentString::undependInternal(Excl
         d.u1.flags = UNDEPENDED_FLAGS | LATIN1_CHARS_BIT;
     else
         d.u1.flags = UNDEPENDED_FLAGS;
 
     return &this->asFlat();
 }
 
 JSFlatString*
-JSDependentString::undepend(ExclusiveContext* cx)
+JSDependentString::undepend(JSContext* cx)
 {
     MOZ_ASSERT(JSString::isDependent());
     return hasLatin1Chars()
            ? undependInternal<Latin1Char>(cx)
            : undependInternal<char16_t>(cx);
 }
 
 #ifdef DEBUG
@@ -1039,16 +1039,55 @@ AutoStableStringChars::copyTwoByteChars(
     chars[length] = 0;
 
     state_ = TwoByte;
     twoByteChars_ = chars;
     s_ = linearString;
     return true;
 }
 
+JSFlatString*
+JSString::ensureFlat(JSContext* cx)
+{
+    if (isFlat())
+        return &asFlat();
+    if (isDependent())
+        return asDependent().undepend(cx);
+    if (isRope())
+        return asRope().flatten(cx);
+    return asExternal().ensureFlat(cx);
+}
+
+JSFlatString*
+JSExternalString::ensureFlat(JSContext* cx)
+{
+    MOZ_ASSERT(hasTwoByteChars());
+
+    size_t n = length();
+    char16_t* s = cx->pod_malloc<char16_t>(n + 1);
+    if (!s)
+        return nullptr;
+
+    // Copy the chars before finalizing the string.
+    {
+        AutoCheckCannotGC nogc;
+        PodCopy(s, nonInlineChars<char16_t>(nogc), n);
+        s[n] = '\0';
+    }
+
+    // Release the external chars.
+    finalize(cx->runtime()->defaultFreeOp());
+
+    // Transform the string into a non-external, flat string.
+    setNonInlineChars<char16_t>(s);
+    d.u1.flags = FLAT_BIT;
+
+    return &this->asFlat();
+}
+
 #ifdef DEBUG
 void
 JSAtom::dump(FILE* fp)
 {
     fprintf(fp, "JSAtom* (%p) = ", (void*) this);
     this->JSString::dump(fp);
 }
 
--- a/js/src/vm/String.h
+++ b/js/src/vm/String.h
@@ -101,23 +101,23 @@ static const size_t UINT32_CHAR_BUFFER_L
  *
  * C++ type                     operations+fields / invariants+properties
  * ==========================   =========================================
  * JSString (abstract)          get(Latin1|TwoByte)CharsZ, get(Latin1|TwoByte)Chars, length / -
  *  | \
  *  | JSRope                    leftChild, rightChild / -
  *  |
  * JSLinearString (abstract)    latin1Chars, twoByteChars / might be null-terminated
- *  | \
- *  | JSDependentString         base / -
+ *  |  |
+ *  |  +-- JSDependentString    base / -
+ *  |  |
+ *  |  +-- JSExternalString     - / char array memory managed by embedding
  *  |
  * JSFlatString                 - / null terminated
  *  |  |
- *  |  +-- JSExternalString     - / char array memory managed by embedding
- *  |  |
  *  |  +-- JSExtensibleString   tracks total buffer capacity (including current text)
  *  |  |
  *  |  +-- JSUndependedString   original dependent base / -
  *  |  |
  *  |  +-- JSInlineString (abstract)    - / chars stored in header
  *  |      |
  *  |      +-- JSThinInlineString       - / header is normal
  *  |      |
@@ -216,22 +216,22 @@ class JSString : public js::gc::TenuredC
      *
      *   String        Instance     Subtype
      *   type          encoding     predicate
      *   ------------------------------------
      *   Rope          000000       000000
      *   Linear        -           !000000
      *   HasBase       -            xxxx1x
      *   Dependent     000010       000010
+     *   External      100000       100000
      *   Flat          -            xxxxx1
      *   Undepended    000011       000011
      *   Extensible    010001       010001
      *   Inline        000101       xxx1xx
      *   FatInline     010101       x1x1xx
-     *   External      100001       100001
      *   Atom          001001       xx1xxx
      *   PermanentAtom 101001       1x1xxx
      *   InlineAtom    -            xx11xx
      *   FatInlineAtom -            x111xx
      *
      * Note that the first 4 flag bits (from right to left in the previous table)
      * have the following meaning and can be used for some hot queries:
      *
@@ -252,17 +252,17 @@ class JSString : public js::gc::TenuredC
     static const uint32_t HAS_BASE_BIT           = JS_BIT(1);
     static const uint32_t INLINE_CHARS_BIT       = JS_BIT(2);
     static const uint32_t ATOM_BIT               = JS_BIT(3);
 
     static const uint32_t ROPE_FLAGS             = 0;
     static const uint32_t DEPENDENT_FLAGS        = HAS_BASE_BIT;
     static const uint32_t UNDEPENDED_FLAGS       = FLAT_BIT | HAS_BASE_BIT;
     static const uint32_t EXTENSIBLE_FLAGS       = FLAT_BIT | JS_BIT(4);
-    static const uint32_t EXTERNAL_FLAGS         = FLAT_BIT | JS_BIT(5);
+    static const uint32_t EXTERNAL_FLAGS         = JS_BIT(5);
 
     static const uint32_t FAT_INLINE_MASK        = INLINE_CHARS_BIT | JS_BIT(4);
     static const uint32_t PERMANENT_ATOM_MASK    = ATOM_BIT | JS_BIT(5);
 
     /* Initial flags for thin inline and fat inline strings. */
     static const uint32_t INIT_THIN_INLINE_FLAGS = FLAT_BIT | INLINE_CHARS_BIT;
     static const uint32_t INIT_FAT_INLINE_FLAGS  = FLAT_BIT | FAT_INLINE_MASK;
 
@@ -347,17 +347,17 @@ class JSString : public js::gc::TenuredC
     }
     bool hasTwoByteChars() const {
         return !(d.u1.flags & LATIN1_CHARS_BIT);
     }
 
     /* Fallible conversions to more-derived string types. */
 
     inline JSLinearString* ensureLinear(js::ExclusiveContext* cx);
-    inline JSFlatString* ensureFlat(js::ExclusiveContext* cx);
+    JSFlatString* ensureFlat(JSContext* cx);
 
     static bool ensureLinear(js::ExclusiveContext* cx, JSString* str) {
         return str->ensureLinear(cx) != nullptr;
     }
 
     /* Type query and debug-checked casts */
 
     MOZ_ALWAYS_INLINE
@@ -600,17 +600,17 @@ static_assert(sizeof(JSRope) == sizeof(J
               "string subclasses must be binary-compatible with JSString");
 
 class JSLinearString : public JSString
 {
     friend class JSString;
     friend class js::AutoStableStringChars;
 
     /* Vacuous and therefore unimplemented. */
-    JSLinearString* ensureLinear(JSContext* cx) = delete;
+    JSLinearString* ensureLinear(js::ExclusiveContext* cx) = delete;
     bool isLinear() const = delete;
     JSLinearString& asLinear() const = delete;
 
   protected:
     /* Returns void pointer to latin1/twoByte chars, for finalizers. */
     MOZ_ALWAYS_INLINE
     void* nonInlineCharsRaw() const {
         MOZ_ASSERT(!isInline());
@@ -680,20 +680,20 @@ class JSLinearString : public JSString
 };
 
 static_assert(sizeof(JSLinearString) == sizeof(JSString),
               "string subclasses must be binary-compatible with JSString");
 
 class JSDependentString : public JSLinearString
 {
     friend class JSString;
-    JSFlatString* undepend(js::ExclusiveContext* cx);
+    JSFlatString* undepend(JSContext* cx);
 
     template <typename CharT>
-    JSFlatString* undependInternal(js::ExclusiveContext* cx);
+    JSFlatString* undependInternal(JSContext* cx);
 
     void init(js::ExclusiveContext* cx, JSLinearString* base, size_t start,
               size_t length);
 
     /* Vacuous and therefore unimplemented. */
     bool isDependent() const = delete;
     JSDependentString& asDependent() const = delete;
 
@@ -908,17 +908,17 @@ class JSFatInlineString : public JSInlin
 
     MOZ_ALWAYS_INLINE void finalize(js::FreeOp* fop);
 };
 
 static_assert(sizeof(JSFatInlineString) % js::gc::CellSize == 0,
               "fat inline strings shouldn't waste space up to the next cell "
               "boundary");
 
-class JSExternalString : public JSFlatString
+class JSExternalString : public JSLinearString
 {
     void init(const char16_t* chars, size_t length, const JSStringFinalizer* fin);
 
     /* Vacuous and therefore unimplemented. */
     bool isExternal() const = delete;
     JSExternalString& asExternal() const = delete;
 
   public:
@@ -937,16 +937,18 @@ class JSExternalString : public JSFlatSt
     const char16_t* twoByteChars() const {
         return rawTwoByteChars();
     }
 
     /* Only called by the GC for strings with the AllocKind::EXTERNAL_STRING kind. */
 
     inline void finalize(js::FreeOp* fop);
 
+    JSFlatString* ensureFlat(JSContext* cx);
+
 #ifdef DEBUG
     void dumpRepresentation(FILE* fp, int indent) const;
 #endif
 };
 
 static_assert(sizeof(JSExternalString) == sizeof(JSString),
               "string subclasses must be binary-compatible with JSString");
 
@@ -1344,26 +1346,16 @@ JSString::getChar(js::ExclusiveContext* 
 MOZ_ALWAYS_INLINE JSLinearString*
 JSString::ensureLinear(js::ExclusiveContext* cx)
 {
     return isLinear()
            ? &asLinear()
            : asRope().flatten(cx);
 }
 
-MOZ_ALWAYS_INLINE JSFlatString*
-JSString::ensureFlat(js::ExclusiveContext* cx)
-{
-    return isFlat()
-           ? &asFlat()
-           : isDependent()
-             ? asDependent().undepend(cx)
-             : asRope().flatten(cx);
-}
-
 inline JSLinearString*
 JSString::base() const
 {
     MOZ_ASSERT(hasBase());
     MOZ_ASSERT(!d.s.u3.base->isInline());
     return d.s.u3.base;
 }
 
--- a/js/xpconnect/src/XPCString.cpp
+++ b/js/xpconnect/src/XPCString.cpp
@@ -36,37 +36,50 @@ XPCStringConvert::FreeZoneCache(JS::Zone
     nsAutoPtr<ZoneStringCache> cache(static_cast<ZoneStringCache*>(JS_GetZoneUserData(zone)));
     JS_SetZoneUserData(zone, nullptr);
 }
 
 // static
 void
 XPCStringConvert::ClearZoneCache(JS::Zone* zone)
 {
+    // Although we clear the cache in FinalizeDOMString if needed, we also clear
+    // the cache here to avoid a dangling JSString* pointer when compacting GC
+    // moves the external string in memory.
+
     ZoneStringCache* cache = static_cast<ZoneStringCache*>(JS_GetZoneUserData(zone));
     if (cache) {
         cache->mBuffer = nullptr;
         cache->mString = nullptr;
     }
 }
 
 // static
 void
-XPCStringConvert::FinalizeLiteral(const JSStringFinalizer* fin, char16_t* chars)
+XPCStringConvert::FinalizeLiteral(JS::Zone* zone, const JSStringFinalizer* fin, char16_t* chars)
 {
 }
 
 const JSStringFinalizer XPCStringConvert::sLiteralFinalizer =
     { XPCStringConvert::FinalizeLiteral };
 
 // static
 void
-XPCStringConvert::FinalizeDOMString(const JSStringFinalizer* fin, char16_t* chars)
+XPCStringConvert::FinalizeDOMString(JS::Zone* zone, const JSStringFinalizer* fin, char16_t* chars)
 {
     nsStringBuffer* buf = nsStringBuffer::FromData(chars);
+
+    // Clear the ZoneStringCache if needed, as this can be called outside GC
+    // when flattening an external string.
+    ZoneStringCache* cache = static_cast<ZoneStringCache*>(JS_GetZoneUserData(zone));
+    if (cache && cache->mBuffer == buf) {
+        cache->mBuffer = nullptr;
+        cache->mString = nullptr;
+    }
+
     buf->Release();
 }
 
 const JSStringFinalizer XPCStringConvert::sDOMStringFinalizer =
     { XPCStringConvert::FinalizeDOMString };
 
 // convert a readable to a JSString, copying string data
 // static
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -273,19 +273,19 @@ public:
     {
         return JS_IsExternalString(str) &&
                JS_GetExternalStringFinalizer(str) == &sDOMStringFinalizer;
     }
 
 private:
     static const JSStringFinalizer sLiteralFinalizer, sDOMStringFinalizer;
 
-    static void FinalizeLiteral(const JSStringFinalizer* fin, char16_t* chars);
+    static void FinalizeLiteral(JS::Zone* zone, const JSStringFinalizer* fin, char16_t* chars);
 
-    static void FinalizeDOMString(const JSStringFinalizer* fin, char16_t* chars);
+    static void FinalizeDOMString(JS::Zone* zone, const JSStringFinalizer* fin, char16_t* chars);
 
     XPCStringConvert() = delete;
 };
 
 class nsIAddonInterposition;
 
 namespace xpc {