Merge inbound to mozilla-central. a=merge
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 21 Feb 2018 12:01:33 +0200
changeset 404632 dd6caba141428343fb26f3ec23a3ee1844a4b241
parent 404631 bb0271610fd8705197ce50d3286db0e5a07fee72 (current diff)
parent 404549 671b0faa7112e687712dff4c99e9e5e788e78f01 (diff)
child 404633 8c32453794b9413cd0cd518717e45f988e55352c
child 404658 c67f82baba03f18239716a4362e0dfe8decee534
push id100044
push userrgurzau@mozilla.com
push dateWed, 21 Feb 2018 10:06:19 +0000
treeherdermozilla-inbound@8c32453794b9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone60.0a1
first release with
nightly linux32
dd6caba14142 / 60.0a1 / 20180221102240 / files
nightly linux64
dd6caba14142 / 60.0a1 / 20180221102240 / files
nightly mac
dd6caba14142 / 60.0a1 / 20180221102240 / files
nightly win32
dd6caba14142 / 60.0a1 / 20180221102240 / files
nightly win64
dd6caba14142 / 60.0a1 / 20180221102240 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to mozilla-central. a=merge
layout/base/nsLayoutUtils.cpp
toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -14,16 +14,18 @@ support-files =
 [browser_0_library_left_pane_migration.js]
 [browser_addBookmarkForFrame.js]
 [browser_bookmark_add_tags.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmark_change_location.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmark_folder_moveability.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
+[browser_bookmark_private_window.js]
+skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmark_remove_tags.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmarklet_windowOpen.js]
 support-files =
   bookmarklet_windowOpen_dummy.html
 [browser_bookmarks_change_title.js]
 skip-if = (os == 'win' && ccov) # Bug 1423667
 [browser_bookmarks_sidebar_search.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_private_window.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that the a website can be bookmarked from a private window.
+ */
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+
+// Cleanup.
+registerCleanupFunction(async () => {
+  await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_add_bookmark_from_private_window() {
+  let win = await BrowserTestUtils.openNewBrowserWindow({private: true});
+  let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL);
+
+  registerCleanupFunction(async () => {
+    await BrowserTestUtils.removeTab(tab);
+    await BrowserTestUtils.closeWindow(win);
+  });
+
+  // Open the bookmark panel.
+  let bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+  let shownPromise = promisePopupShown(bookmarkPanel);
+  let bookmarkStar = win.BookmarkingUI.star;
+  bookmarkStar.click();
+  await shownPromise;
+
+  // Check if the bookmark star changes its state after click.
+  Assert.equal(bookmarkStar.getAttribute("starred"), "true", "Bookmark star changed its state correctly.");
+
+  // Close the bookmark panel.
+  let hiddenPromise = promisePopupHidden(bookmarkPanel);
+  let doneButton = win.document.getElementById("editBookmarkPanelDoneButton");
+  doneButton.click();
+  await hiddenPromise;
+
+  let bm = await PlacesUtils.bookmarks.fetch({url: TEST_URL});
+  Assert.equal(bm.url, TEST_URL, "The bookmark was successfully saved in the database.");
+});
--- a/browser/modules/ThemeVariableMap.jsm
+++ b/browser/modules/ThemeVariableMap.jsm
@@ -18,12 +18,9 @@ const ThemeVariableMap = [
   ["--urlbar-separator-color", "toolbar_field_separator"],
   ["--tabs-border-color", "toolbar_top_separator", "navigator-toolbox"],
   ["--lwt-toolbar-vertical-separator", "toolbar_vertical_separator"],
   ["--toolbox-border-bottom-color", "toolbar_bottom_separator"],
   ["--lwt-toolbarbutton-icon-fill", "icon_color"],
   ["--lwt-toolbarbutton-icon-fill-attention", "icon_attention_color"],
   ["--lwt-toolbarbutton-hover-background", "button_background_hover"],
   ["--lwt-toolbarbutton-active-background", "button_background_active"],
-  ["--arrowpanel-background", "popup"],
-  ["--arrowpanel-color", "popup_text"],
-  ["--arrowpanel-border-color", "popup_border"],
 ];
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -946,28 +946,16 @@ panelmultiview .toolbaritem-combined-but
 
 #appMenu-zoom-controls > .subviewbutton {
   margin-inline-start: 10px;
 }
 
 /* Unset the min-height constraint, because that works better for a text-only button. */
 #appMenu-zoomReset-button {
   min-height: unset;
-  border: 1px solid var(--panel-separator-color);
-  border-radius: 10000px;
-  padding: 1px 8px;
-  background-color: var(--arrowpanel-dimmed);
-}
-
-#appMenu-zoomReset-button@buttonStateHover@ {
-  background-color: var(--arrowpanel-dimmed-further);
-}
-
-#appMenu-zoomReset-button@buttonStateActive@ {
-  background-color: var(--arrowpanel-dimmed-even-further);
 }
 
 #appMenu-zoomReset-button > .toolbarbutton-text {
   min-width: calc(3ch + 8px);
   text-align: center;
 }
 
 .toolbaritem-combined-buttons > toolbarseparator[orient="vertical"] + .subviewbutton,
@@ -975,16 +963,24 @@ panelmultiview .toolbaritem-combined-but
   margin-inline-start: 0;
 }
 
 .PanelUI-subView .toolbaritem-combined-buttons > .subviewbutton-iconic > .toolbarbutton-text,
 .PanelUI-subView .toolbaritem-combined-buttons > .subviewbutton:not(.subviewbutton-iconic) > .toolbarbutton-icon {
   display: none;
 }
 
+/* Using this selector, because this way the hover and active selectors will apply properly. */
+.PanelUI-subView .toolbaritem-combined-buttons > .subviewbutton:not(.subviewbutton-iconic) {
+  background-color: #f9f9f9;
+  border: 1px solid #e1e1e1;
+  border-radius: 10000px;
+  padding: 1px 8px;
+}
+
 .toolbaritem-combined-buttons > .subviewbutton:not(.subviewbutton-iconic) > .toolbarbutton-text {
   font-size: 1em;
   padding-inline-start: 0;
 }
 
 .subview-subheader {
   color: GrayText;
 }
--- a/dom/geolocation/nsGeolocation.cpp
+++ b/dom/geolocation/nsGeolocation.cpp
@@ -73,16 +73,17 @@ class nsGeolocationRequest final
 
   NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsGeolocationRequest, nsIContentPermissionRequest)
 
   nsGeolocationRequest(Geolocation* aLocator,
                        GeoPositionCallback aCallback,
                        GeoPositionErrorCallback aErrorCallback,
                        UniquePtr<PositionOptions>&& aOptions,
                        uint8_t aProtocolType,
+                       nsIEventTarget* aMainThreadTarget,
                        bool aWatchPositionRequest = false,
                        bool aIsHandlingUserInput = false,
                        int32_t aWatchId = 0);
 
   MOZ_DECLARE_WEAKREFERENCE_TYPENAME(nsGeolocationRequest)
 
   void Shutdown();
 
@@ -131,16 +132,17 @@ class nsGeolocationRequest final
   bool mIsHandlingUserInput;
 
   RefPtr<Geolocation> mLocator;
 
   int32_t mWatchId;
   bool mShutdown;
   nsCOMPtr<nsIContentPermissionRequester> mRequester;
   uint8_t mProtocolType;
+  nsCOMPtr<nsIEventTarget> mMainThreadTarget;
 };
 
 static UniquePtr<PositionOptions>
 CreatePositionOptionsCopy(const PositionOptions& aOptions)
 {
   UniquePtr<PositionOptions> geoOptions = MakeUnique<PositionOptions>();
 
   geoOptions->mEnableHighAccuracy = aOptions.mEnableHighAccuracy;
@@ -302,28 +304,30 @@ PositionError::NotifyCallback(const GeoP
 // nsGeolocationRequest
 ////////////////////////////////////////////////////
 
 nsGeolocationRequest::nsGeolocationRequest(Geolocation* aLocator,
                                            GeoPositionCallback aCallback,
                                            GeoPositionErrorCallback aErrorCallback,
                                            UniquePtr<PositionOptions>&& aOptions,
                                            uint8_t aProtocolType,
+                                           nsIEventTarget* aMainThreadTarget,
                                            bool aWatchPositionRequest,
                                            bool aIsHandlingUserInput,
                                            int32_t aWatchId)
   : mIsWatchPositionRequest(aWatchPositionRequest),
     mCallback(Move(aCallback)),
     mErrorCallback(Move(aErrorCallback)),
     mOptions(Move(aOptions)),
     mIsHandlingUserInput(aIsHandlingUserInput),
     mLocator(aLocator),
     mWatchId(aWatchId),
     mShutdown(false),
-    mProtocolType(aProtocolType)
+    mProtocolType(aProtocolType),
+    mMainThreadTarget(aMainThreadTarget)
 {
   if (nsCOMPtr<nsPIDOMWindowInner> win =
       do_QueryReferent(mLocator->GetOwner())) {
     mRequester = new nsContentPermissionRequester(win);
   }
 }
 
 nsGeolocationRequest::~nsGeolocationRequest()
@@ -616,17 +620,17 @@ nsGeolocationRequest::GetPrincipal()
   }
   return mLocator->GetPrincipal();
 }
 
 NS_IMETHODIMP
 nsGeolocationRequest::Update(nsIDOMGeoPosition* aPosition)
 {
   nsCOMPtr<nsIRunnable> ev = new RequestSendLocationEvent(aPosition, this);
-  NS_DispatchToMainThread(ev);
+  mMainThreadTarget->Dispatch(ev.forget());
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsGeolocationRequest::NotifyError(uint16_t aErrorCode)
 {
   MOZ_ASSERT(NS_IsMainThread());
   RefPtr<PositionError> positionError = new PositionError(mLocator, aErrorCode);
@@ -1242,41 +1246,51 @@ Geolocation::GetCurrentPosition(Position
                                    Move(CreatePositionOptionsCopy(aOptions)),
                                    aCallerType);
 
   if (NS_FAILED(rv)) {
     aRv.Throw(rv);
   }
 }
 
+static nsIEventTarget* MainThreadTarget(Geolocation* geo)
+{
+  nsCOMPtr<nsPIDOMWindowInner> window = do_QueryReferent(geo->GetOwner());
+  if (!window) {
+    return GetMainThreadEventTarget();
+  }
+  return nsGlobalWindowInner::Cast(window)->EventTargetFor(mozilla::TaskCategory::Other);
+}
+
 nsresult
 Geolocation::GetCurrentPosition(GeoPositionCallback callback,
                                 GeoPositionErrorCallback errorCallback,
                                 UniquePtr<PositionOptions>&& options,
                                 CallerType aCallerType)
 {
   if (mPendingCallbacks.Length() > MAX_GEO_REQUESTS_PER_WINDOW) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   // After this we hand over ownership of options to our nsGeolocationRequest.
 
   // Count the number of requests per protocol/scheme.
   Telemetry::Accumulate(Telemetry::GEOLOCATION_GETCURRENTPOSITION_SECURE_ORIGIN,
                         static_cast<uint8_t>(mProtocolType));
 
+  nsIEventTarget* target = MainThreadTarget(this);
   RefPtr<nsGeolocationRequest> request =
     new nsGeolocationRequest(this, Move(callback), Move(errorCallback),
-                             Move(options), static_cast<uint8_t>(mProtocolType),
+                             Move(options), static_cast<uint8_t>(mProtocolType), target,
                              false, EventStateManager::IsHandlingUserInput());
 
   if (!sGeoEnabled || ShouldBlockInsecureRequests() ||
       nsContentUtils::ResistFingerprinting(aCallerType)) {
     nsCOMPtr<nsIRunnable> ev = new RequestAllowEvent(false, request);
-    NS_DispatchToMainThread(ev);
+    target->Dispatch(ev.forget());
     return NS_OK;
   }
 
   if (!mOwner && aCallerType != CallerType::System) {
     return NS_ERROR_FAILURE;
   }
 
   if (mOwner) {
@@ -1287,17 +1301,17 @@ Geolocation::GetCurrentPosition(GeoPosit
     return NS_OK;
   }
 
   if (aCallerType != CallerType::System) {
     return NS_ERROR_FAILURE;
   }
 
   nsCOMPtr<nsIRunnable> ev = new RequestAllowEvent(true, request);
-  NS_DispatchToMainThread(ev);
+  target->Dispatch(ev.forget());
 
   return NS_OK;
 }
 
 int32_t
 Geolocation::WatchPosition(PositionCallback& aCallback,
                            PositionErrorCallback* aErrorCallback,
                            const PositionOptions& aOptions,
@@ -1345,26 +1359,27 @@ Geolocation::WatchPosition(GeoPositionCa
 
   // Count the number of requests per protocol/scheme.
   Telemetry::Accumulate(Telemetry::GEOLOCATION_WATCHPOSITION_SECURE_ORIGIN,
                         static_cast<uint8_t>(mProtocolType));
 
   // The watch ID:
   *aRv = mLastWatchId++;
 
+  nsIEventTarget* target = MainThreadTarget(this);
   RefPtr<nsGeolocationRequest> request =
     new nsGeolocationRequest(this, Move(aCallback), Move(aErrorCallback),
                              Move(aOptions),
-                             static_cast<uint8_t>(mProtocolType), true,
+                             static_cast<uint8_t>(mProtocolType), target, true,
                              EventStateManager::IsHandlingUserInput(), *aRv);
 
   if (!sGeoEnabled || ShouldBlockInsecureRequests() ||
       nsContentUtils::ResistFingerprinting(aCallerType)) {
     nsCOMPtr<nsIRunnable> ev = new RequestAllowEvent(false, request);
-    NS_DispatchToMainThread(ev);
+    target->Dispatch(ev.forget());
     return NS_OK;
   }
 
   if (!mOwner && aCallerType != CallerType::System) {
     return NS_ERROR_FAILURE;
   }
 
   if (mOwner) {
@@ -1448,25 +1463,26 @@ Geolocation::NotifyAllowedRequest(nsGeol
   } else {
     mPendingCallbacks.AppendElement(aRequest);
   }
 }
 
 bool
 Geolocation::RegisterRequestWithPrompt(nsGeolocationRequest* request)
 {
+  nsIEventTarget* target = MainThreadTarget(this);
   if (Preferences::GetBool("geo.prompt.testing", false)) {
     bool allow = Preferences::GetBool("geo.prompt.testing.allow", false);
     nsCOMPtr<nsIRunnable> ev = new RequestAllowEvent(allow, request);
-    NS_DispatchToMainThread(ev);
+    target->Dispatch(ev.forget());
     return true;
   }
 
   nsCOMPtr<nsIRunnable> ev  = new RequestPromptEvent(request, mOwner);
-  NS_DispatchToMainThread(ev);
+  target->Dispatch(ev.forget());
   return true;
 }
 
 JSObject*
 Geolocation::WrapObject(JSContext *aCtx, JS::Handle<JSObject*> aGivenProto)
 {
   return GeolocationBinding::Wrap(aCtx, this, aGivenProto);
 }
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -497,17 +497,22 @@ Promise::ReportRejectedPromise(JSContext
                                : GetCurrentThreadWorkerPrivate()->IsChromeWorker();
   nsGlobalWindowInner* win = isMainThread
     ? xpc::WindowGlobalOrNull(aPromise)
     : nullptr;
   xpcReport->Init(report.report(), report.toStringResult().c_str(), isChrome,
                   win ? win->AsInner()->WindowID() : 0);
 
   // Now post an event to do the real reporting async
-  NS_DispatchToMainThread(new AsyncErrorReporter(xpcReport));
+  RefPtr<nsIRunnable> event = new AsyncErrorReporter(xpcReport);
+  if (win) {
+    win->Dispatch(mozilla::TaskCategory::Other, event.forget());
+  } else {
+    NS_DispatchToMainThread(event);
+  }
 }
 
 bool
 Promise::PerformMicroTaskCheckpoint()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!");
 
   CycleCollectedJSContext* context = CycleCollectedJSContext::Get();
--- a/dom/xml/nsXMLContentSink.cpp
+++ b/dom/xml/nsXMLContentSink.cpp
@@ -1610,16 +1610,16 @@ nsXMLContentSink::ContinueInterruptedPar
 void
 nsXMLContentSink::ContinueInterruptedParsingAsync()
 {
   nsCOMPtr<nsIRunnable> ev =
     NewRunnableMethod("nsXMLContentSink::ContinueInterruptedParsingIfEnabled",
                       this,
                       &nsXMLContentSink::ContinueInterruptedParsingIfEnabled);
 
-  NS_DispatchToCurrentThread(ev);
+  mDocument->Dispatch(mozilla::TaskCategory::Other, ev.forget());
 }
 
 nsIParser*
 nsXMLContentSink::GetParser()
 {
   return static_cast<nsIParser*>(mParser.get());
 }
--- a/gfx/src/nsThebesFontEnumerator.cpp
+++ b/gfx/src/nsThebesFontEnumerator.cpp
@@ -126,44 +126,47 @@ private:
     nsCOMPtr<nsIThread> mWorkerThread;
 };
 
 class EnumerateFontsTask final : public Runnable
 {
 public:
     EnumerateFontsTask(nsAtom* aLangGroupAtom,
                        const nsAutoCString& aGeneric,
-                       UniquePtr<EnumerateFontsPromise> aEnumerateFontsPromise)
+                       UniquePtr<EnumerateFontsPromise> aEnumerateFontsPromise,
+                       nsIEventTarget* aMainThreadTarget)
         : Runnable("EnumerateFontsTask")
         , mLangGroupAtom(aLangGroupAtom)
         , mGeneric(aGeneric)
         , mEnumerateFontsPromise(Move(aEnumerateFontsPromise))
+        , mMainThreadTarget(aMainThreadTarget)
     {
         MOZ_ASSERT(NS_IsMainThread());
     }
 
     NS_IMETHOD Run() override
     {
         MOZ_ASSERT(!NS_IsMainThread());
 
         nsTArray<nsString> fontList;
 
         nsresult rv = gfxPlatform::GetPlatform()->
             GetFontList(mLangGroupAtom, mGeneric, fontList);
         nsCOMPtr<nsIRunnable> runnable = new EnumerateFontsResult(
             rv, Move(mEnumerateFontsPromise), Move(fontList));
-        NS_DispatchToMainThread(runnable.forget());
+        mMainThreadTarget->Dispatch(runnable.forget());
 
         return NS_OK;
     }
 
 private:
     RefPtr<nsAtom> mLangGroupAtom;
     nsAutoCStringN<16> mGeneric;
     UniquePtr<EnumerateFontsPromise> mEnumerateFontsPromise;
+    RefPtr<nsIEventTarget> mMainThreadTarget;
 };
 
 NS_IMETHODIMP
 nsThebesFontEnumerator::EnumerateAllFontsAsync(JSContext* aCx,
                                                JS::MutableHandleValue aRval)
 {
     return EnumerateFontsAsync(nullptr, nullptr, aCx, aRval);
 }
@@ -202,18 +205,19 @@ nsThebesFontEnumerator::EnumerateFontsAs
 
     nsAutoCString generic;
     if (aGeneric) {
         generic.Assign(aGeneric);
     } else {
         generic.SetIsVoid(true);
     }
 
+    nsCOMPtr<nsIEventTarget> target = global->EventTargetFor(mozilla::TaskCategory::Other);
     nsCOMPtr<nsIRunnable> runnable = new EnumerateFontsTask(
-        langGroupAtom, generic, Move(enumerateFontsPromise));
+        langGroupAtom, generic, Move(enumerateFontsPromise), target);
     thread->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
 
     if (!ToJSValue(aCx, promise, aRval)) {
         return NS_ERROR_FAILURE;
     }
 
     return NS_OK;
 }
--- a/js/src/fuzz-tests/moz.build
+++ b/js/src/fuzz-tests/moz.build
@@ -6,16 +6,21 @@
 
 GeckoProgram('fuzz-tests', linkage=None)
 
 UNIFIED_SOURCES += [
     'testExample.cpp',
     'tests.cpp',
 ]
 
+if CONFIG['JS_BUILD_BINAST']:
+    UNIFIED_SOURCES += [
+        'testBinASTReader.cpp',
+    ]
+
 DEFINES['EXPORT_JS_API'] = True
 
 LOCAL_INCLUDES += [
     '!..',
     '..',
 ]
 
 if CONFIG['ENABLE_INTL_API'] and CONFIG['MOZ_ICU_DATA_ARCHIVE']:
new file mode 100644
--- /dev/null
+++ b/js/src/fuzz-tests/testBinASTReader.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ */
+/* 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/. */
+
+
+#include "mozilla/ScopeExit.h"
+
+#include "jsapi.h"
+
+#include "frontend/BinSource.h"
+#include "frontend/FullParseHandler.h"
+#include "frontend/ParseContext.h"
+#include "frontend/Parser.h"
+#include "fuzz-tests/tests.h"
+#include "vm/Interpreter.h"
+
+#include "vm/JSContext-inl.h"
+
+using UsedNameTracker = js::frontend::UsedNameTracker;
+using namespace js;
+
+// These are defined and pre-initialized by the harness (in tests.cpp).
+extern JS::PersistentRootedObject gGlobal;
+extern JSContext* gCx;
+
+static int
+testBinASTReaderInit(int *argc, char ***argv) {
+  return 0;
+}
+
+static int
+testBinASTReaderFuzz(const uint8_t* buf, size_t size) {
+    auto gcGuard = mozilla::MakeScopeExit([&] {
+        JS::PrepareForFullGC(gCx);
+        JS::GCForReason(gCx, GC_NORMAL, JS::gcreason::API);
+    });
+
+    if (!size) return 0;
+
+    CompileOptions options(gCx);
+    options.setIntroductionType("fuzzing parse")
+       .setFileAndLine("<string>", 1);
+
+    js::Vector<uint8_t> binSource(gCx);
+    if (!binSource.append(buf, size)) {
+        ReportOutOfMemory(gCx);
+        return 0;
+    }
+
+    js::frontend::UsedNameTracker binUsedNames(gCx);
+    if (!binUsedNames.init()) {
+        ReportOutOfMemory(gCx);
+        return 0;
+    }
+
+    js::frontend::BinASTParser reader(gCx, gCx->tempLifoAlloc(), binUsedNames, options);
+
+    // Will be deallocated once `reader` goes out of scope.
+    auto binParsed = reader.parse(binSource);
+    RootedValue binExn(gCx);
+    if (binParsed.isErr()) {
+        js::GetAndClearException(gCx, &binExn);
+        return 0;
+    }
+
+#if defined(DEBUG) // Dumping an AST is only defined in DEBUG builds
+    Sprinter binPrinter(gCx);
+    if (!binPrinter.init()) {
+        ReportOutOfMemory(gCx);
+        return 0;
+    }
+    DumpParseTree(binParsed.unwrap(), binPrinter);
+#endif // defined(DEBUG)
+
+    return 0;
+}
+
+MOZ_FUZZING_INTERFACE_RAW(testBinASTReaderInit, testBinASTReaderFuzz, BinAST);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/cacheir/shape-teleporting-1.js
@@ -0,0 +1,128 @@
+// Receiver shadows
+(function() {
+    function check(p) { return p.x; }
+
+    let a = { x: "a" };
+    let b = { __proto__: a };
+    let c = { __proto__: b };
+    let d = { __proto__: c };
+
+    assertEq(check(d), "a");
+    assertEq(check(d), "a");
+    d.x = "d";
+    assertEq(check(d), "d");
+})();
+
+// Intermediate proto shadows
+(function() {
+    function check(p) { return p.x; }
+
+    let a = { x: "a" };
+    let b = { __proto__: a };
+    let c = { __proto__: b };
+    let d = { __proto__: c };
+
+    assertEq(check(d), "a");
+    assertEq(check(d), "a");
+    c.x = "c";
+    assertEq(check(d), "c");
+})();
+
+// Receiver proto changes
+(function() {
+    function check(p) { return p.x; }
+
+    let a = { x: "a" };
+    let b = { __proto__: a };
+    let c = { __proto__: b };
+    let d = { __proto__: c };
+
+    assertEq(check(d), "a");
+    assertEq(check(d), "a");
+    d.__proto__ = { x: "?" };
+    assertEq(check(d), "?");
+})();
+
+// Intermediate proto changes
+(function() {
+    function check(p) { return p.x; }
+
+    let a = { x: "a" };
+    let b = { __proto__: a };
+    let c = { __proto__: b };
+    let d = { __proto__: c };
+
+    assertEq(check(d), "a");
+    assertEq(check(d), "a");
+    c.__proto__ = { x: "?" };
+    assertEq(check(d), "?");
+})();
+
+// Uncacheable holder proto
+(function() {
+    function check(p) { return p.x; }
+
+    function Base() { this.x = "a"; }
+    let a = new Base;
+    a.__proto__ = new Object;
+    let b = { __proto__: a };
+    let c = { __proto__: b };
+    let d = { __proto__: c };
+
+    assertEq(check(d), "a");
+    assertEq(check(d), "a");
+    b.__proto__ = { x: "?" };
+    assertEq(check(d), "?");
+})();
+
+// Uncacheable intermediate proto
+(function() {
+    function check(p) { return p.x; }
+
+    function Base() { this.x = "a"; }
+    function Node() { }
+
+    let a = new Base;
+    let b = new Node; b.__proto__ = a;
+    let c = { __proto__: b };
+    let d = { __proto__: c };
+
+    assertEq(check(d), "a");
+    assertEq(check(d), "a");
+    b.__proto__ = { x: "?" };
+    assertEq(check(d), "?");
+})();
+
+// Uncacheable receiver proto
+(function() {
+    function check(p) { return p.x; }
+
+    function Base() { this.x = "a"; }
+    function Node() { }
+
+    let a = new Base;
+    let b = { __proto__: a };
+    let c = { __proto__: b };
+    let d = new Node; d.__proto__ = c;
+
+    assertEq(check(d), "a");
+    assertEq(check(d), "a");
+    d.__proto__ = { x: "?" };
+    assertEq(check(d), "?");
+})();
+
+// Uncacheable receiver proto (only receiver / holder)
+(function() {
+    function check(p) { return p.x; }
+
+    function Base() { this.x = "a"; }
+    function Node() { }
+
+    let a = new Base;
+    let b = new Node; b.__proto__ = a;
+
+    assertEq(check(b), "a");
+    assertEq(check(b), "a");
+    b.__proto__ = { x: "?" };
+    assertEq(check(b), "?");
+})();
--- a/js/src/jit/CacheIR.cpp
+++ b/js/src/jit/CacheIR.cpp
@@ -521,65 +521,142 @@ CanAttachNativeGetProp(JSContext* cx, Ha
         if (IsCacheableGetPropCallNative(obj, holder, shape))
             return CanAttachCallGetter;
     }
 
     return CanAttachNone;
 }
 
 static void
+GuardGroupProto(CacheIRWriter& writer, JSObject* obj, ObjOperandId objId)
+{
+    // Uses the group to determine if the prototype is unchanged. If the
+    // group's prototype is mutable, we must check the actual prototype,
+    // otherwise checking the group is sufficient. This can be used if object
+    // is not ShapedObject or if Shape has UNCACHEABLE_PROTO flag set.
+
+    ObjectGroup* group = obj->groupRaw();
+
+    if (group->hasUncacheableProto())
+        writer.guardProto(objId, obj->staticPrototype());
+    else
+        writer.guardGroupForProto(objId, group);
+}
+
+static void
 GeneratePrototypeGuards(CacheIRWriter& writer, JSObject* obj, JSObject* holder, ObjOperandId objId)
 {
-    // The guards here protect against the effects of JSObject::swap(). If the
-    // prototype chain is directly altered, then TI will toss the jitcode, so we
-    // don't have to worry about it, and any other change to the holder, or
-    // adding a shadowing property will result in reshaping the holder, and thus
-    // the failure of the shape guard.
+    // Assuming target property is on |holder|, generate appropriate guards to
+    // ensure |holder| is still on the prototype chain of |obj| and we haven't
+    // introduced any shadowing definitions.
+    //
+    // For each item in the proto chain before holder, we must ensure that
+    // [[GetPrototypeOf]] still has the expected result, and that
+    // [[GetOwnProperty]] has no definition of the target property.
+    //
+    //
+    // Shape Teleporting Optimization
+    // ------------------------------
+    //
+    // Starting with the assumption (and guideline to developers) that mutating
+    // prototypes is an uncommon and fair-to-penalize operation we move cost
+    // from the access side to the mutation side.
+    //
+    // Consider the following proto chain, with B defining a property 'x':
+    //
+    //      D  ->  C  ->  B{x: 3}  ->  A  -> null
+    //
+    // When accessing |D.x| we refer to D as the "receiver", and B as the
+    // "holder". To optimize this access we need to ensure that neither D nor C
+    // has since defined a shadowing property 'x'. Since C is a prototype that
+    // we assume is rarely mutated we would like to avoid checking each time if
+    // new properties are added. To do this we require that everytime C is
+    // mutated that in addition to generating a new shape for itself, it will
+    // walk the proto chain and generate new shapes for those objects on the
+    // chain (B and A). As a result, checking the shape of D and B is
+    // sufficient. Note that we do not care if the shape or properties of A
+    // change since the lookup of 'x' will stop at B.
+    //
+    // The second condition we must verify is that the prototype chain was not
+    // mutated. The same mechanism as above is used. When the prototype link is
+    // changed, we generate a new shape for the object. If the object whose
+    // link we are mutating is itself a prototype, we regenerate shapes down
+    // the chain. This means the same two shape checks as above are sufficient.
+    //
+    // Unfortunately we don't stop there and add further caveats. We may set
+    // the UNCACHEABLE_PROTO flag on the shape of an object to indicate that it
+    // will not generate a new shape if its prototype link is modified. If the
+    // object is itself a prototype we follow the shape chain and regenerate
+    // shapes (if they aren't themselves uncacheable).
+    //
+    // Let's consider the effect of the UNCACHEABLE_PROTO flag on our example:
+    // - D is uncacheable: Add check that D still links to C
+    // - C is uncacheable: Modifying C.__proto__ will still reshape B (if B is
+    //                     not uncacheable)
+    // - B is uncacheable: Add shape check C since B will not reshape OR check
+    //                     proto of D and C
+    //
+    // See:
+    //  - ReshapeForProtoMutation
+    //  - ReshapeForShadowedProp
+
+    MOZ_ASSERT(holder);
     MOZ_ASSERT(obj != holder);
 
-    if (obj->hasUncacheableProto()) {
-        // If the shape does not imply the proto, emit an explicit proto guard.
-        writer.guardProto(objId, obj->staticPrototype());
+    // Only DELEGATE objects participate in teleporting so peel off the first
+    // object in the chain if needed and handle it directly.
+    JSObject* pobj = obj;
+    if (!obj->isDelegate()) {
+        if (obj->hasUncacheableProto())
+            GuardGroupProto(writer, obj, objId);
+
+        pobj = obj->staticPrototype();
     }
-
-    JSObject* pobj = obj->staticPrototype();
-    if (!pobj)
+    MOZ_ASSERT(pobj->isDelegate());
+
+    // In the common case, holder has a cacheable prototype and will regenerate
+    // its shape if any (delegate) objects in the proto chain are updated.
+    if (!holder->hasUncacheableProto())
+        return;
+
+    // If already at the holder, no further proto checks are needed.
+    if (pobj == holder)
         return;
 
+    // NOTE: We could be clever and look for a middle prototype to shape check
+    //       and elide some (but not all) of the group checks. Unless we have
+    //       real-world examples, let's avoid the complexity.
+
+    // Synchronize pobj and protoId.
+    MOZ_ASSERT(pobj == obj || pobj == obj->staticPrototype());
+    ObjOperandId protoId = (pobj == obj) ? objId
+                                         : writer.loadProto(objId);
+
+    // Guard prototype links from |pobj| to |holder|.
     while (pobj != holder) {
-        if (pobj->hasUncacheableProto()) {
-            ObjOperandId protoId = writer.loadObject(pobj);
-            if (pobj->isSingleton()) {
-                // Singletons can have their group's |proto| mutated directly.
-                writer.guardProto(protoId, pobj->staticPrototype());
-            } else {
-                writer.guardGroup(protoId, pobj->group());
-            }
-        }
         pobj = pobj->staticPrototype();
+        protoId = writer.loadProto(protoId);
+
+        writer.guardSpecificObject(protoId, pobj);
     }
 }
 
 static void
 GeneratePrototypeHoleGuards(CacheIRWriter& writer, JSObject* obj, ObjOperandId objId)
 {
-    if (obj->hasUncacheableProto()) {
-        // If the shape does not imply the proto, emit an explicit proto guard.
-        writer.guardProto(objId, obj->staticPrototype());
-    }
+    if (obj->hasUncacheableProto())
+        GuardGroupProto(writer, obj, objId);
 
     JSObject* pobj = obj->staticPrototype();
     while (pobj) {
         ObjOperandId protoId = writer.loadObject(pobj);
 
-        // Non-singletons with uncacheable protos can change their proto
-        // without a shape change, so also guard on the group (which determines
-        // the proto) in this case.
-        if (pobj->hasUncacheableProto() && !pobj->isSingleton())
-            writer.guardGroup(protoId, pobj->group());
+        // If shape doesn't imply proto, additional guards are needed.
+        if (pobj->hasUncacheableProto())
+            GuardGroupProto(writer, pobj, protoId);
 
         // Make sure the shape matches, to avoid non-dense elements or anything
         // else that is being checked by CanAttachDenseElementHole.
         writer.guardShape(protoId, pobj->as<NativeObject>().lastProperty());
 
         // Also make sure there are no dense elements.
         writer.guardNoDenseElements(protoId);
 
@@ -587,55 +664,61 @@ GeneratePrototypeHoleGuards(CacheIRWrite
     }
 }
 
 static void
 TestMatchingReceiver(CacheIRWriter& writer, JSObject* obj, ObjOperandId objId,
                      Maybe<ObjOperandId>* expandoId)
 {
     if (obj->is<UnboxedPlainObject>()) {
-        writer.guardGroup(objId, obj->group());
+        writer.guardGroupForLayout(objId, obj->group());
 
         if (UnboxedExpandoObject* expando = obj->as<UnboxedPlainObject>().maybeExpando()) {
             expandoId->emplace(writer.guardAndLoadUnboxedExpando(objId));
             writer.guardShape(expandoId->ref(), expando->lastProperty());
         } else {
             writer.guardNoUnboxedExpando(objId);
         }
     } else if (obj->is<TypedObject>()) {
-        writer.guardGroup(objId, obj->group());
+        writer.guardGroupForLayout(objId, obj->group());
     } else {
         Shape* shape = obj->maybeShape();
         MOZ_ASSERT(shape);
         writer.guardShape(objId, shape);
     }
 }
 
 static void
 EmitReadSlotGuard(CacheIRWriter& writer, JSObject* obj, JSObject* holder,
                   ObjOperandId objId, Maybe<ObjOperandId>* holderId)
 {
     Maybe<ObjOperandId> expandoId;
     TestMatchingReceiver(writer, obj, objId, &expandoId);
 
     if (obj != holder) {
-        GeneratePrototypeGuards(writer, obj, holder, objId);
-
         if (holder) {
+            // Guard proto chain integrity.
+            GeneratePrototypeGuards(writer, obj, holder, objId);
+
             // Guard on the holder's shape.
             holderId->emplace(writer.loadObject(holder));
             writer.guardShape(holderId->ref(), holder->as<NativeObject>().lastProperty());
         } else {
             // The property does not exist. Guard on everything in the prototype
             // chain. This is guaranteed to see only Native objects because of
             // CanAttachNativeGetProp().
             JSObject* proto = obj->taggedProto().toObjectOrNull();
             ObjOperandId lastObjId = objId;
             while (proto) {
                 ObjOperandId protoId = writer.loadProto(lastObjId);
+
+                // If shape doesn't imply proto, additional guards are needed.
+                if (proto->hasUncacheableProto())
+                    GuardGroupProto(writer, proto, lastObjId);
+
                 writer.guardShape(protoId, proto->as<NativeObject>().lastProperty());
                 proto = proto->staticPrototype();
                 lastObjId = protoId;
             }
         }
     } else if (obj->is<UnboxedPlainObject>()) {
         holderId->emplace(*expandoId);
     } else {
@@ -1324,17 +1407,17 @@ GetPropIRGenerator::tryAttachUnboxed(Han
     const UnboxedLayout::Property* property = obj->as<UnboxedPlainObject>().layout().lookup(id);
     if (!property)
         return false;
 
     if (!cx_->runtime()->jitSupportsFloatingPoint)
         return false;
 
     maybeEmitIdGuard(id);
-    writer.guardGroup(objId, obj->group());
+    writer.guardGroupForLayout(objId, obj->group());
     writer.loadUnboxedPropertyResult(objId, property->type,
                                      UnboxedPlainObject::offsetOfData() + property->offset);
     if (property->type == JSVAL_TYPE_OBJECT)
         writer.typeMonitorResult();
     else
         writer.returnFromIC();
 
     preliminaryObjectAction_ = PreliminaryObjectAction::Unlink;
@@ -2417,20 +2500,20 @@ HasPropIRGenerator::tryAttachSparse(Hand
     }
 
     // Guard that this is a native object.
     writer.guardIsNativeObject(objId);
 
     // Generate prototype guards if needed. This includes monitoring that
     // properties were not added in the chain.
     if (!hasOwn) {
-        if (!obj->hasUncacheableProto()) {
-            // Make sure the proto does not change without checking the shape.
-            writer.guardProto(objId, obj->staticPrototype());
-        }
+        // If GeneratePrototypeHoleGuards below won't add guards for prototype,
+        // we should add our own since we aren't guarding shape.
+        if (!obj->hasUncacheableProto())
+            GuardGroupProto(writer, obj, objId);
 
         GeneratePrototypeHoleGuards(writer, obj, objId);
     }
 
     // Because of the prototype guard we know that the prototype chain
     // does not include any dense or sparse (i.e indexed) properties.
     writer.callObjectHasSparseElementResult(objId, indexId);
     writer.returnFromIC();
@@ -2516,17 +2599,17 @@ HasPropIRGenerator::tryAttachUnboxed(JSO
     if (!obj->is<UnboxedPlainObject>())
         return false;
 
     const UnboxedLayout::Property* prop = obj->as<UnboxedPlainObject>().layout().lookup(key);
     if (!prop)
         return false;
 
     emitIdGuard(keyId, key);
-    writer.guardGroup(objId, obj->group());
+    writer.guardGroupForLayout(objId, obj->group());
     writer.loadBooleanResult(true);
     writer.returnFromIC();
 
     trackAttached("UnboxedHasProp");
     return true;
 }
 
 bool
@@ -2583,17 +2666,17 @@ HasPropIRGenerator::tryAttachTypedObject
 {
     if (!obj->is<TypedObject>())
         return false;
 
     if (!obj->as<TypedObject>().typeDescr().hasProperty(cx_->names(), key))
         return false;
 
     emitIdGuard(keyId, key);
-    writer.guardGroup(objId, obj->group());
+    writer.guardGroupForLayout(objId, obj->group());
     writer.loadBooleanResult(true);
     writer.returnFromIC();
 
     trackAttached("TypedObjectHasProp");
     return true;
 }
 
 bool
@@ -2936,17 +3019,17 @@ SetPropIRGenerator::tryAttachNativeSetSl
 
     maybeEmitIdGuard(id);
 
     // If we need a property type barrier (always in Baseline, sometimes in
     // Ion), guard on both the shape and the group. If Ion knows the property
     // types match, we don't need the group guard.
     NativeObject* nobj = &obj->as<NativeObject>();
     if (typeCheckInfo_.needsTypeBarrier())
-        writer.guardGroup(objId, nobj->group());
+        writer.guardGroupForTypeBarrier(objId, nobj->group());
     writer.guardShape(objId, nobj->lastProperty());
 
     if (IsPreliminaryObject(obj))
         preliminaryObjectAction_ = PreliminaryObjectAction::NotePreliminary;
     else
         preliminaryObjectAction_ = PreliminaryObjectAction::Unlink;
 
     typeCheckInfo_.set(nobj->group(), id);
@@ -2967,17 +3050,17 @@ SetPropIRGenerator::tryAttachUnboxedExpa
     if (!expando)
         return false;
 
     Shape* propShape = LookupShapeForSetSlot(JSOp(*pc_), expando, id);
     if (!propShape)
         return false;
 
     maybeEmitIdGuard(id);
-    writer.guardGroup(objId, obj->group());
+    writer.guardGroupForLayout(objId, obj->group());
     ObjOperandId expandoId = writer.guardAndLoadUnboxedExpando(objId);
     writer.guardShape(expandoId, expando->lastProperty());
 
     // Property types must be added to the unboxed object's group, not the
     // expando's group (it has unknown properties).
     typeCheckInfo_.set(obj->group(), id);
     EmitStoreSlotAndReturn(writer, expandoId, expando, propShape, rhsId);
 
@@ -3003,17 +3086,17 @@ SetPropIRGenerator::tryAttachUnboxedProp
     if (!obj->is<UnboxedPlainObject>() || !cx_->runtime()->jitSupportsFloatingPoint)
         return false;
 
     const UnboxedLayout::Property* property = obj->as<UnboxedPlainObject>().layout().lookup(id);
     if (!property)
         return false;
 
     maybeEmitIdGuard(id);
-    writer.guardGroup(objId, obj->group());
+    writer.guardGroupForLayout(objId, obj->group());
     EmitGuardUnboxedPropertyType(writer, property->type, rhsId);
     writer.storeUnboxedProperty(objId, property->type,
                                 UnboxedPlainObject::offsetOfData() + property->offset,
                                 rhsId);
     writer.returnFromIC();
 
     typeCheckInfo_.set(obj->group(), id);
     preliminaryObjectAction_ = PreliminaryObjectAction::Unlink;
@@ -3045,17 +3128,17 @@ SetPropIRGenerator::tryAttachTypedObject
         return false;
 
     uint32_t fieldOffset = structDescr->fieldOffset(fieldIndex);
     TypedThingLayout layout = GetTypedThingLayout(obj->getClass());
 
     maybeEmitIdGuard(id);
     writer.guardNoDetachedTypedObjects();
     writer.guardShape(objId, obj->as<TypedObject>().shape());
-    writer.guardGroup(objId, obj->group());
+    writer.guardGroupForLayout(objId, obj->group());
 
     typeCheckInfo_.set(obj->group(), id);
 
     // Scalar types can always be stored without a type update stub.
     if (fieldDescr->is<ScalarTypeDescr>()) {
         Scalar::Type type = fieldDescr->as<ScalarTypeDescr>().type();
         writer.storeTypedObjectScalarProperty(objId, fieldOffset, layout, type, rhsId);
         writer.returnFromIC();
@@ -3268,17 +3351,17 @@ SetPropIRGenerator::tryAttachSetDenseEle
     if (!obj->isNative())
         return false;
 
     NativeObject* nobj = &obj->as<NativeObject>();
     if (!nobj->containsDenseElement(index) || nobj->getElementsHeader()->isFrozen())
         return false;
 
     if (typeCheckInfo_.needsTypeBarrier())
-        writer.guardGroup(objId, nobj->group());
+        writer.guardGroupForTypeBarrier(objId, nobj->group());
     writer.guardShape(objId, nobj->shape());
 
     writer.storeDenseElement(objId, indexId, rhsId);
     writer.returnFromIC();
 
     // Type inference uses JSID_VOID for the element types.
     typeCheckInfo_.set(nobj->group(), JSID_VOID);
 
@@ -3326,20 +3409,18 @@ CanAttachAddElement(JSObject* obj, bool 
 
     return true;
 }
 
 static void
 ShapeGuardProtoChain(CacheIRWriter& writer, JSObject* obj, ObjOperandId objId)
 {
     while (true) {
-        // Guard on the proto if the shape does not imply the proto. Singleton
-        // objects always trigger a shape change when the proto changes, so we
-        // don't need a guard in that case.
-        bool guardProto = obj->hasUncacheableProto() && !obj->isSingleton();
+        // Guard on the proto if the shape does not imply the proto.
+        bool guardProto = obj->hasUncacheableProto();
 
         obj = obj->staticPrototype();
         if (!obj)
             return;
 
         objId = writer.loadProto(objId);
         if (guardProto)
             writer.guardSpecificObject(objId, obj);
@@ -3386,17 +3467,17 @@ SetPropIRGenerator::tryAttachSetDenseEle
     if (nobj->is<TypedArrayObject>())
         return false;
 
     // Check for other indexed properties or class hooks.
     if (!CanAttachAddElement(nobj, IsPropertyInitOp(op)))
         return false;
 
     if (typeCheckInfo_.needsTypeBarrier())
-        writer.guardGroup(objId, nobj->group());
+        writer.guardGroupForTypeBarrier(objId, nobj->group());
     writer.guardShape(objId, nobj->shape());
 
     // Also shape guard the proto chain, unless this is an INITELEM or we know
     // the proto chain has no indexed props.
     if (IsPropertySetOp(op) && maybeHasExtraIndexedProps_)
         ShapeGuardProtoChain(writer, obj, objId);
 
     writer.storeDenseElementHole(objId, indexId, rhsId, isAdd);
@@ -3565,17 +3646,17 @@ SetPropIRGenerator::tryAttachDOMProxyExp
     if (CanAttachNativeSetSlot(cx_, JSOp(*pc_), expandoObj, id, isTemporarilyUnoptimizable_,
                                &propShape))
     {
         maybeEmitIdGuard(id);
         ObjOperandId expandoObjId =
             guardDOMProxyExpandoObjectAndShape(obj, objId, expandoVal, expandoObj);
 
         NativeObject* nativeExpandoObj = &expandoObj->as<NativeObject>();
-        writer.guardGroup(expandoObjId, nativeExpandoObj->group());
+        writer.guardGroupForTypeBarrier(expandoObjId, nativeExpandoObj->group());
         typeCheckInfo_.set(nativeExpandoObj->group(), id);
 
         EmitStoreSlotAndReturn(writer, expandoObjId, nativeExpandoObj, propShape, rhsId);
         trackAttached("DOMProxyExpandoSlot");
         return true;
     }
 
     RootedObject holder(cx_);
@@ -3712,17 +3793,17 @@ SetPropIRGenerator::tryAttachWindowProxy
     }
 
     maybeEmitIdGuard(id);
 
     writer.guardClass(objId, GuardClassKind::WindowProxy);
     ObjOperandId windowObjId = writer.loadObject(windowObj);
 
     writer.guardShape(windowObjId, windowObj->lastProperty());
-    writer.guardGroup(windowObjId, windowObj->group());
+    writer.guardGroupForTypeBarrier(windowObjId, windowObj->group());
     typeCheckInfo_.set(windowObj->group(), id);
 
     EmitStoreSlotAndReturn(writer, windowObjId, windowObj, propShape, rhsId);
 
     trackAttached("WindowProxySlot");
     return true;
 }
 
@@ -3848,17 +3929,20 @@ SetPropIRGenerator::tryAttachAddSlotStub
         {
             return false;
         }
     }
 
     ObjOperandId objId = writer.guardIsObject(objValId);
     maybeEmitIdGuard(id);
 
-    writer.guardGroup(objId, oldGroup);
+    // In addition to guarding for type barrier, we need this group guard (or
+    // shape guard below) to ensure class is unchanged.
+    MOZ_ASSERT(!oldGroup->hasUncacheableClass() || obj->is<ShapedObject>());
+    writer.guardGroupForTypeBarrier(objId, oldGroup);
 
     // If we are adding a property to an object for which the new script
     // properties analysis hasn't been performed yet, make sure the stub fails
     // after we run the analysis as a group change may be required here. The
     // group change is not required for correctness but improves type
     // information elsewhere.
     if (oldGroup->newScript() && !oldGroup->newScript()->analyzed()) {
         writer.guardGroupHasUnanalyzedNewScript(oldGroup);
@@ -4250,17 +4334,17 @@ CallIRGenerator::tryAttachArrayPush()
 
     // This is a soft assert, documenting the fact that we pass 'true'
     // for needsTypeBarrier when constructing typeCheckInfo_ for CallIRGenerator.
     // Can be removed safely if the assumption becomes false.
     MOZ_ASSERT(typeCheckInfo_.needsTypeBarrier());
 
     // Guard that the group and shape matches.
     if (typeCheckInfo_.needsTypeBarrier())
-        writer.guardGroup(thisObjId, thisobj->group());
+        writer.guardGroupForTypeBarrier(thisObjId, thisobj->group());
     writer.guardShape(thisObjId, thisarray->shape());
 
     // Guard proto chain shapes.
     ShapeGuardProtoChain(writer, thisobj, thisObjId);
 
     // arr.push(x) is equivalent to arr[arr.length] = x for regular arrays.
     ValOperandId argId = writer.loadStackValue(0);
     writer.arrayPush(thisObjId, argId);
@@ -4678,9 +4762,9 @@ GetIntrinsicIRGenerator::trackAttached(c
 
 bool
 GetIntrinsicIRGenerator::tryAttachStub()
 {
     writer.loadValueResult(val_);
     writer.returnFromIC();
     trackAttached("GetIntrinsic");
     return true;
-}
\ No newline at end of file
+}
--- a/js/src/jit/CacheIR.h
+++ b/js/src/jit/CacheIR.h
@@ -552,20 +552,42 @@ class MOZ_RAII CacheIRWriter : public JS
         buffer_.writeByte(uint32_t(!!shapeWrapper));        addStubField(uintptr_t(shapeWrapper), StubField::Type::JSObject);
     }
     // Guard rhs[slot] == prototypeObject
     void guardFunctionPrototype(ObjOperandId rhs, uint32_t slot, ObjOperandId protoId) {
         writeOpWithOperandId(CacheOp::GuardFunctionPrototype, rhs);
         writeOperandId(protoId);
         addStubField(slot, StubField::Type::RawWord);
     }
+  private:
+    // Use (or create) a specialization below to clarify what constaint the
+    // group guard is implying.
     void guardGroup(ObjOperandId obj, ObjectGroup* group) {
         writeOpWithOperandId(CacheOp::GuardGroup, obj);
         addStubField(uintptr_t(group), StubField::Type::ObjectGroup);
     }
+  public:
+    void guardGroupForProto(ObjOperandId obj, ObjectGroup* group) {
+        MOZ_ASSERT(!group->hasUncacheableProto());
+        guardGroup(obj, group);
+    }
+    void guardGroupForTypeBarrier(ObjOperandId obj, ObjectGroup* group) {
+        // Typesets will always be a super-set of any typesets previously seen
+        // for this group. If the type/group of a value being stored to a
+        // property in this group is not known, a TypeUpdate IC chain should be
+        // used as well.
+        guardGroup(obj, group);
+    }
+    void guardGroupForLayout(ObjOperandId obj, ObjectGroup* group) {
+        // NOTE: Comment in guardGroupForTypeBarrier also applies.
+        MOZ_ASSERT(!group->hasUncacheableClass());
+        MOZ_ASSERT(IsUnboxedObjectClass(group->clasp()) ||
+                   IsTypedObjectClass(group->clasp()));
+        guardGroup(obj, group);
+    }
     void guardProto(ObjOperandId obj, JSObject* proto) {
         writeOpWithOperandId(CacheOp::GuardProto, obj);
         addStubField(uintptr_t(proto), StubField::Type::JSObject);
     }
     void guardClass(ObjOperandId obj, GuardClassKind kind) {
         static_assert(sizeof(GuardClassKind) == sizeof(uint8_t),
                       "GuardClassKind must fit in a byte");
         writeOpWithOperandId(CacheOp::GuardClass, obj);
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -671,16 +671,22 @@ if CONFIG['JS_BUILD_BINAST']:
     # specification.
     SOURCES += ['frontend/BinTokenReaderTester.cpp']
     # These parts of BinAST should eventually move to release.
     SOURCES += [
         'frontend/BinSource.cpp',
         'frontend/BinToken.cpp'
     ]
 
+    # Instrument BinAST files for fuzzing as we have a fuzzing target for BinAST.
+    if CONFIG['FUZZING_INTERFACES'] and CONFIG['LIBFUZZER']:
+        SOURCES['frontend/BinSource.cpp'].flags += ['-fsanitize-coverage=trace-pc-guard']
+        SOURCES['frontend/BinToken.cpp'].flags += ['-fsanitize-coverage=trace-pc-guard']
+        SOURCES['frontend/BinTokenReaderTester.cpp'].flags += ['-fsanitize-coverage=trace-pc-guard']
+
 # Wasm code should use WASM_HUGE_MEMORY instead of JS_CODEGEN_X64
 # so that it is easy to use the huge-mapping optimization for other
 # 64-bit platforms in the future.
 
 if CONFIG['JS_CODEGEN_X64']:
     DEFINES['WASM_HUGE_MEMORY'] = True
 
 if CONFIG['MOZ_DEBUG'] or CONFIG['NIGHTLY_BUILD']:
--- a/js/src/vm/EnvironmentObject.cpp
+++ b/js/src/vm/EnvironmentObject.cpp
@@ -1022,17 +1022,17 @@ LexicalEnvironmentObject::clone(JSContex
 {
     Rooted<LexicalScope*> scope(cx, &env->scope());
     RootedObject enclosing(cx, &env->enclosingEnvironment());
     Rooted<LexicalEnvironmentObject*> copy(cx, create(cx, scope, enclosing, gc::DefaultHeap));
     if (!copy)
         return nullptr;
 
     // We can't assert that the clone has the same shape, because it could
-    // have been reshaped by PurgeEnvironmentChain.
+    // have been reshaped by ReshapeForShadowedProp.
     MOZ_ASSERT(env->slotSpan() == copy->slotSpan());
     for (uint32_t i = JSSLOT_FREE(&class_); i < copy->slotSpan(); i++)
         copy->setSlot(i, env->getSlot(i));
 
     return copy;
 }
 
 /* static */ LexicalEnvironmentObject*
--- a/js/src/vm/JSObject.cpp
+++ b/js/src/vm/JSObject.cpp
@@ -1659,16 +1659,23 @@ JSObject::swap(JSContext* cx, HandleObje
     // Watch for oddball objects that have special organizational issues and
     // can't be swapped.
     MOZ_ASSERT(!a->is<RegExpObject>() && !b->is<RegExpObject>());
     MOZ_ASSERT(!a->is<ArrayObject>() && !b->is<ArrayObject>());
     MOZ_ASSERT(!a->is<ArrayBufferObject>() && !b->is<ArrayBufferObject>());
     MOZ_ASSERT(!a->is<TypedArrayObject>() && !b->is<TypedArrayObject>());
     MOZ_ASSERT(!a->is<TypedObject>() && !b->is<TypedObject>());
 
+    // Don't swap objects that may currently be participating in shape
+    // teleporting optimizations.
+    //
+    // See: ReshapeForProtoMutation, ReshapeForShadowedProp
+    MOZ_ASSERT_IF(a->isNative() && a->isDelegate(), a->taggedProto() == TaggedProto());
+    MOZ_ASSERT_IF(b->isNative() && b->isDelegate(), b->taggedProto() == TaggedProto());
+
     bool aIsProxyWithInlineValues =
         a->is<ProxyObject>() && a->as<ProxyObject>().usingInlineValueArray();
     bool bIsProxyWithInlineValues =
         b->is<ProxyObject>() && b->as<ProxyObject>().usingInlineValueArray();
 
     if (a->tenuredSizeOfThis() == b->tenuredSizeOfThis()) {
         // When both objects are the same size, just do a plain swap of their
         // contents.
@@ -1982,57 +1989,73 @@ JSObject::fixupAfterMovingGC()
             if (owner != &obj && owner->hasFixedElements())
                 obj.elements_ = owner->getElementsHeader()->elements();
             MOZ_ASSERT(!IsForwarded(obj.getElementsHeader()->ownerObject().get()));
         }
     }
 }
 
 static bool
+ReshapeForProtoMutation(JSContext* cx, HandleObject obj)
+{
+    // To avoid the JIT guarding on each prototype in chain to detect prototype
+    // mutation, we can instead reshape the rest of the proto chain such that a
+    // guard on any of them is sufficient. To avoid excessive reshaping and
+    // invalidation, we apply heuristics to decide when to apply this and when
+    // to require a guard.
+    //
+    // Heuristics:
+    //  - Always reshape singleton objects. This historically avoided
+    //    de-optimizing in cases that compiler doesn't support
+    //    uncacheable-proto. TODO: Revisit if this is a good idea.
+    //  - Other objects instead set UNCACHEABLE_PROTO flag on shape to avoid
+    //    creating too many private shape copies.
+    //  - Only propegate along proto chain if we are mark DELEGATE. This avoids
+    //    reshaping in normal object access cases.
+    //
+    // NOTE: We only handle NativeObjects and don't propegate reshapes through
+    //       any non-native objects on the chain.
+    //
+    // See Also:
+    //  - GeneratePrototypeGuards
+    //  - GeneratePrototypeHoleGuards
+    //  - ObjectGroup::defaultNewGroup
+
+    RootedObject pobj(cx, obj);
+
+    while (pobj && pobj->isNative()) {
+        if (pobj->isSingleton()) {
+            // If object was converted to a singleton it should have cleared
+            // any UNCACHEABLE_PROTO flags.
+            MOZ_ASSERT(!pobj->hasUncacheableProto());
+
+            if (!NativeObject::reshapeForProtoMutation(cx, pobj.as<NativeObject>()))
+                return false;
+        } else {
+            if (!JSObject::setUncacheableProto(cx, pobj))
+                return false;
+        }
+
+        if (!obj->isDelegate())
+            break;
+
+        pobj = pobj->staticPrototype();
+    }
+
+    return true;
+}
+
+static bool
 SetClassAndProto(JSContext* cx, HandleObject obj,
                  const Class* clasp, Handle<js::TaggedProto> proto)
 {
-    // Regenerate the object's shape. If the object is a proto (isDelegate()),
-    // we also need to regenerate shapes for all of the objects along the old
-    // prototype chain, in case any entries were filled by looking up through
-    // obj. Stop when a non-native object is found, prototype lookups will not
-    // be cached across these.
-    //
-    // How this shape change is done is very delicate; the change can be made
-    // either by marking the object's prototype as uncacheable (such that the
-    // JIT'ed ICs cannot assume the shape determines the prototype) or by just
-    // generating a new shape for the object. Choosing the former is bad if the
-    // object is on the prototype chain of other objects, as the uncacheable
-    // prototype can inhibit iterator caches on those objects and slow down
-    // prototype accesses. Choosing the latter is bad if there are many similar
-    // objects to this one which will have their prototype mutated, as the
-    // generateOwnShape forces the object into dictionary mode and similar
-    // property lineages will be repeatedly cloned.
-    //
-    // :XXX: bug 707717 make this code less brittle.
-    RootedObject oldproto(cx, obj);
-    while (oldproto && oldproto->isNative()) {
-        if (oldproto->isSingleton()) {
-            // We always generate a new shape if the object is a singleton,
-            // regardless of the uncacheable-proto flag. ICs may rely on
-            // this.
-            if (!NativeObject::generateOwnShape(cx, oldproto.as<NativeObject>()))
-                return false;
-        } else {
-            if (!JSObject::setUncacheableProto(cx, oldproto))
-                return false;
-        }
-        if (!obj->isDelegate()) {
-            // If |obj| is not a proto of another object, we don't need to
-            // reshape the whole proto chain.
-            MOZ_ASSERT(obj == oldproto);
-            break;
-        }
-        oldproto = oldproto->staticPrototype();
-    }
+    // Regenerate object shape (and possibly prototype shape) to invalidate JIT
+    // code that is affected by a prototype mutation.
+    if (!ReshapeForProtoMutation(cx, obj))
+        return false;
 
     if (proto.isObject()) {
         RootedObject protoObj(cx, proto.toObject());
         if (!JSObject::setDelegate(cx, protoObj))
             return false;
     }
 
     if (obj->isSingleton()) {
--- a/js/src/vm/JSObject.h
+++ b/js/src/vm/JSObject.h
@@ -174,25 +174,19 @@ class JSObject : public js::gc::Cell
         GENERATE_NONE,
         GENERATE_SHAPE
     };
 
     static bool setFlags(JSContext* cx, JS::HandleObject obj, js::BaseShape::Flag flags,
                          GenerateShape generateShape = GENERATE_NONE);
     inline bool hasAllFlags(js::BaseShape::Flag flags) const;
 
-    /*
-     * An object is a delegate if it is on another object's prototype or scope
-     * chain, and therefore the delegate might be asked implicitly to get or
-     * set a property on behalf of another object. Delegates may be accessed
-     * directly too, as may any object, but only those objects linked after the
-     * head of any prototype or scope chain are flagged as delegates. This
-     * definition helps to optimize shape-based property cache invalidation
-     * (see Purge{Scope,Proto}Chain in JSObject.cpp).
-     */
+    // An object is a delegate if it is on another object's prototype or
+    // environment chain. Optimization heuristics will make use of this flag.
+    // See: ReshapeForProtoMutation, ReshapeForShadowedProp
     inline bool isDelegate() const;
     static bool setDelegate(JSContext* cx, JS::HandleObject obj) {
         return setFlags(cx, obj, js::BaseShape::DELEGATE, GENERATE_SHAPE);
     }
 
     inline bool isBoundFunction() const;
     inline bool hasSpecialEquality() const;
 
--- a/js/src/vm/NativeObject.cpp
+++ b/js/src/vm/NativeObject.cpp
@@ -1348,17 +1348,17 @@ PurgeProtoChain(JSContext* cx, JSObject*
     RootedShape shape(cx);
     while (obj) {
         /* Lookups will not be cached through non-native protos. */
         if (!obj->isNative())
             break;
 
         shape = obj->as<NativeObject>().lookup(cx, id);
         if (shape)
-            return NativeObject::shadowingShapeChange(cx, obj.as<NativeObject>(), *shape);
+            return NativeObject::reshapeForShadowedProp(cx, obj.as<NativeObject>());
 
         obj = obj->staticPrototype();
     }
 
     return true;
 }
 
 static bool
@@ -1390,40 +1390,52 @@ PurgeEnvironmentChainHelper(JSContext* c
                 return false;
         }
     }
 
     return true;
 }
 
 /*
- * PurgeEnvironmentChain does nothing if obj is not itself a prototype or
+ * ReshapeForShadowedProp does nothing if obj is not itself a prototype or
  * parent environment, else it reshapes the scope and prototype chains it
  * links. It calls PurgeEnvironmentChainHelper, which asserts that obj is
  * flagged as a delegate (i.e., obj has ever been on a prototype or parent
  * chain).
  */
 static MOZ_ALWAYS_INLINE bool
-PurgeEnvironmentChain(JSContext* cx, HandleObject obj, HandleId id)
+ReshapeForShadowedProp(JSContext* cx, HandleObject obj, HandleId id)
 {
     if (obj->isDelegate() && obj->isNative())
         return PurgeEnvironmentChainHelper(cx, obj, id);
     return true;
 }
 
+/* static */ bool
+NativeObject::reshapeForShadowedProp(JSContext* cx, HandleNativeObject obj)
+{
+    return generateOwnShape(cx, obj);
+}
+
+/* static */ bool
+NativeObject::reshapeForProtoMutation(JSContext* cx, HandleNativeObject obj)
+{
+    return generateOwnShape(cx, obj);
+}
+
 enum class IsAddOrChange { Add, AddOrChange };
 
 template <IsAddOrChange AddOrChange>
 static MOZ_ALWAYS_INLINE bool
 AddOrChangeProperty(JSContext* cx, HandleNativeObject obj, HandleId id,
                     Handle<PropertyDescriptor> desc)
 {
     desc.assertComplete();
 
-    if (!PurgeEnvironmentChain(cx, obj, id))
+    if (!ReshapeForShadowedProp(cx, obj, id))
         return false;
 
     // Use dense storage for new indexed properties where possible.
     if (JSID_IS_INT(id) &&
         !desc.getter() &&
         !desc.setter() &&
         desc.attributes() == JSPROP_ENUMERATE &&
         (!obj->isIndexed() || !obj->containsPure(id)) &&
@@ -1489,17 +1501,17 @@ AddOrChangeProperty(JSContext* cx, Handl
 // Version of AddOrChangeProperty optimized for adding a plain data property.
 // This function doesn't handle integer ids as we may have to store them in
 // dense elements.
 static MOZ_ALWAYS_INLINE bool
 AddDataProperty(JSContext* cx, HandleNativeObject obj, HandleId id, HandleValue v)
 {
     MOZ_ASSERT(!JSID_IS_INT(id));
 
-    if (!PurgeEnvironmentChain(cx, obj, id))
+    if (!ReshapeForShadowedProp(cx, obj, id))
         return false;
 
     Shape* shape = NativeObject::addEnumerableDataProperty(cx, obj, id);
     if (!shape)
         return false;
 
     UpdateShapeTypeAndValueForWritableDataProp(cx, obj, shape, id, v);
 
@@ -2562,17 +2574,17 @@ js::SetPropertyByDefining(JSContext* cx,
 
             // Step 5.e.ii.
             if (!desc.writable())
                 return result.fail(JSMSG_READ_ONLY);
         }
     }
 
     // Purge the property cache of now-shadowed id in receiver's environment chain.
-    if (!PurgeEnvironmentChain(cx, receiver, id))
+    if (!ReshapeForShadowedProp(cx, receiver, id))
         return false;
 
     // Steps 5.e.iii-iv. and 5.f.i. Define the new data property.
     unsigned attrs =
         existing
         ? JSPROP_IGNORE_ENUMERATE | JSPROP_IGNORE_READONLY | JSPROP_IGNORE_PERMANENT
         : JSPROP_ENUMERATE;
     return DefineDataProperty(cx, receiver, id, v, attrs, result);
@@ -2626,17 +2638,17 @@ SetNonexistentProperty(JSContext* cx, Ha
             MOZ_ASSERT(!desc.object());
         }
 #endif
 
         // Step 5.e. Define the new data property.
 
         if (DefinePropertyOp op = obj->getOpsDefineProperty()) {
             // Purge the property cache of now-shadowed id in receiver's environment chain.
-            if (!PurgeEnvironmentChain(cx, obj, id))
+            if (!ReshapeForShadowedProp(cx, obj, id))
                 return false;
 
             Rooted<PropertyDescriptor> desc(cx);
             desc.initFields(nullptr, v, JSPROP_ENUMERATE, nullptr, nullptr);
 
             MOZ_ASSERT(!cx->helperThread());
             return op(cx, obj, id, desc, result);
         }
--- a/js/src/vm/NativeObject.h
+++ b/js/src/vm/NativeObject.h
@@ -687,18 +687,18 @@ class NativeObject : public ShapedObject
     }
 
     static MOZ_MUST_USE bool generateOwnShape(JSContext* cx, HandleNativeObject obj,
                                               Shape* newShape = nullptr)
     {
         return replaceWithNewEquivalentShape(cx, obj, obj->lastProperty(), newShape);
     }
 
-    static MOZ_MUST_USE bool shadowingShapeChange(JSContext* cx, HandleNativeObject obj,
-                                                  const Shape& shape);
+    static MOZ_MUST_USE bool reshapeForShadowedProp(JSContext* cx, HandleNativeObject obj);
+    static MOZ_MUST_USE bool reshapeForProtoMutation(JSContext* cx, HandleNativeObject obj);
     static bool clearFlag(JSContext* cx, HandleNativeObject obj, BaseShape::Flag flag);
 
     // The maximum number of slots in an object.
     // |MAX_SLOTS_COUNT * sizeof(JS::Value)| shouldn't overflow
     // int32_t (see slotsSizeMustNotOverflow).
     static const uint32_t MAX_SLOTS_COUNT = (1 << 28) - 1;
 
     static void slotsSizeMustNotOverflow() {
--- a/js/src/vm/ObjectGroup.cpp
+++ b/js/src/vm/ObjectGroup.cpp
@@ -531,16 +531,24 @@ ObjectGroup::defaultNewGroup(JSContext* 
 
         // Objects which are prototypes of one another should be singletons, so
         // that their type information can be tracked more precisely. Limit
         // this group change to plain objects, to avoid issues with other types
         // of singletons like typed arrays.
         if (protoObj->is<PlainObject>() && !protoObj->isSingleton()) {
             if (!JSObject::changeToSingleton(cx, protoObj))
                 return nullptr;
+
+            // |ReshapeForProtoMutation| ensures singletons will reshape when
+            // prototype is mutated so clear the UNCACHEABLE_PROTO flag.
+            if (protoObj->hasUncacheableProto()) {
+                HandleNativeObject nobj = protoObj.as<NativeObject>();
+                if (!NativeObject::clearFlag(cx, nobj, BaseShape::UNCACHEABLE_PROTO))
+                    return nullptr;
+            }
         }
     }
 
     ObjectGroupCompartment::NewTable::AddPtr p =
         table->lookupForAdd(ObjectGroupCompartment::NewEntry::Lookup(clasp, proto, associated));
     if (p) {
         ObjectGroup* group = p->group;
         MOZ_ASSERT_IF(clasp, group->clasp() == clasp);
--- a/js/src/vm/ObjectGroup.h
+++ b/js/src/vm/ObjectGroup.h
@@ -98,34 +98,52 @@ class ObjectGroup : public gc::TenuredCe
 
   public:
     const Class* clasp() const {
         return clasp_;
     }
 
     void setClasp(const Class* clasp) {
         MOZ_ASSERT(JS::StringIsASCII(clasp->name));
+        MOZ_ASSERT(hasUncacheableClass());
         clasp_ = clasp;
     }
 
+    // Certain optimizations may mutate the class of an ObjectGroup - and thus
+    // all objects in it - after it is created. If true, the JIT must not
+    // assume objects of a previously seen group have the same class as before.
+    //
+    // See: TryConvertToUnboxedLayout
+    bool hasUncacheableClass() const {
+        return clasp_->isNative();
+    }
+
     bool hasDynamicPrototype() const {
         return proto_.isDynamic();
     }
 
     const GCPtr<TaggedProto>& proto() const {
         return proto_;
     }
 
     GCPtr<TaggedProto>& proto() {
         return proto_;
     }
 
     void setProto(TaggedProto proto);
     void setProtoUnchecked(TaggedProto proto);
 
+    bool hasUncacheableProto() const {
+        // We allow singletons to mutate their prototype after the group has
+        // been created. If true, the JIT must re-check prototype even if group
+        // has been seen before.
+        MOZ_ASSERT(!hasDynamicPrototype());
+        return singleton();
+    }
+
     bool singleton() const {
         return flagsDontCheckGeneration() & OBJECT_FLAG_SINGLETON;
     }
 
     bool lazy() const {
         bool res = flagsDontCheckGeneration() & OBJECT_FLAG_LAZY_SINGLETON;
         MOZ_ASSERT_IF(res, singleton());
         return res;
--- a/js/src/vm/Shape.cpp
+++ b/js/src/vm/Shape.cpp
@@ -89,17 +89,17 @@ Shape::removeFromDictionary(NativeObject
 
     obj->shape()->clearCachedBigEnoughForShapeTable();
 }
 
 void
 Shape::insertIntoDictionary(GCPtrShape* dictp)
 {
     // Don't assert inDictionaryMode() here because we may be called from
-    // JSObject::toDictionaryMode via JSObject::newDictionaryShape.
+    // NativeObject::toDictionaryMode via Shape::initDictionaryShape.
     MOZ_ASSERT(inDictionary());
     MOZ_ASSERT(!listp);
 
     MOZ_ASSERT_IF(*dictp, (*dictp)->inDictionary());
     MOZ_ASSERT_IF(*dictp, (*dictp)->listp == dictp);
     MOZ_ASSERT_IF(*dictp, zone() == (*dictp)->zone());
 
     setParent(dictp->get());
@@ -1309,22 +1309,16 @@ NativeObject::replaceWithNewEquivalentSh
         oldShape->handoffTableTo(newShape);
 
     if (entry)
         entry->setPreservingCollision(newShape);
     return newShape;
 }
 
 /* static */ bool
-NativeObject::shadowingShapeChange(JSContext* cx, HandleNativeObject obj, const Shape& shape)
-{
-    return generateOwnShape(cx, obj);
-}
-
-/* static */ bool
 JSObject::setFlags(JSContext* cx, HandleObject obj, BaseShape::Flag flags,
                    GenerateShape generateShape)
 {
     if (obj->hasAllFlags(flags))
         return true;
 
     if (obj->isNative() && obj->as<NativeObject>().inDictionaryMode()) {
         if (generateShape == GENERATE_SHAPE) {
@@ -1354,19 +1348,22 @@ JSObject::setFlags(JSContext* cx, Handle
     obj->as<ShapedObject>().setShape(newShape);
 
     return true;
 }
 
 /* static */ bool
 NativeObject::clearFlag(JSContext* cx, HandleNativeObject obj, BaseShape::Flag flag)
 {
-    MOZ_ASSERT(obj->inDictionaryMode());
+    MOZ_ASSERT(obj->lastProperty()->getObjectFlags() & flag);
 
-    MOZ_ASSERT(obj->lastProperty()->getObjectFlags() & flag);
+    if (!obj->inDictionaryMode()) {
+        if (!toDictionaryMode(cx, obj))
+            return false;
+    }
 
     StackBaseShape base(obj->lastProperty());
     base.flags &= ~flag;
     UnownedBaseShape* nbase = BaseShape::getUnowned(cx, base);
     if (!nbase)
         return false;
 
     obj->lastProperty()->base()->adoptUnowned(nbase);
--- a/js/src/vm/UnboxedObject.h
+++ b/js/src/vm/UnboxedObject.h
@@ -312,16 +312,23 @@ class UnboxedPlainObject : public Unboxe
         return offsetOfShapeOrExpando();
     }
 
     static size_t offsetOfData() {
         return offsetof(UnboxedPlainObject, data_[0]);
     }
 };
 
+inline bool
+IsUnboxedObjectClass(const Class* class_)
+{
+    return class_ == &UnboxedPlainObject::class_;
+}
+
+
 // Try to construct an UnboxedLayout for each of the preliminary objects,
 // provided they all match the template shape. If successful, converts the
 // preliminary objects and their group to the new unboxed representation.
 bool
 TryConvertToUnboxedLayout(JSContext* cx, AutoEnterAnalysis& enter, Shape* templateShape,
                           ObjectGroup* group, PreliminaryObjectArray* objects);
 
 } // namespace js
--- a/js/src/wasm/WasmAST.h
+++ b/js/src/wasm/WasmAST.h
@@ -1137,34 +1137,32 @@ class AstConversionOperator final : publ
       : AstExpr(Kind, ExprType::Limit),
         op_(op), operand_(operand)
     {}
 
     Op op() const { return op_; }
     AstExpr* operand() const { return operand_; }
 };
 
-#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
 // Like AstConversionOperator, but for opcodes encoded with the Numeric prefix.
 class AstExtraConversionOperator final : public AstExpr
 {
     NumericOp op_;
     AstExpr* operand_;
 
   public:
     static const AstExprKind Kind = AstExprKind::ExtraConversionOperator;
     explicit AstExtraConversionOperator(NumericOp op, AstExpr* operand)
       : AstExpr(Kind, ExprType::Limit),
         op_(op), operand_(operand)
     {}
 
     NumericOp op() const { return op_; }
     AstExpr* operand() const { return operand_; }
 };
-#endif
 
 // This is an artificial AST node which can fill operand slots in an AST
 // constructed from parsing or decoding stack-machine code that doesn't have
 // an inherent AST structure.
 class AstPop final : public AstExpr
 {
   public:
     static const AstExprKind Kind = AstExprKind::Pop;
--- a/js/src/wasm/WasmBinaryToText.cpp
+++ b/js/src/wasm/WasmBinaryToText.cpp
@@ -1349,22 +1349,20 @@ RenderExpr(WasmRenderContext& c, AstExpr
       case AstExprKind::ComparisonOperator:
         if (!RenderComparisonOperator(c, expr.as<AstComparisonOperator>()))
             return false;
         break;
       case AstExprKind::ConversionOperator:
         if (!RenderConversionOperator(c, expr.as<AstConversionOperator>()))
             return false;
         break;
-#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
       case AstExprKind::ExtraConversionOperator:
         if (!RenderExtraConversionOperator(c, expr.as<AstExtraConversionOperator>()))
             return false;
         break;
-#endif
       case AstExprKind::Load:
         if (!RenderLoad(c, expr.as<AstLoad>()))
             return false;
         break;
       case AstExprKind::Store:
         if (!RenderStore(c, expr.as<AstStore>()))
             return false;
         break;
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -8091,95 +8091,124 @@ nsLayoutUtils::GetFontFacesForFrames(nsI
     aFrame = GetNextContinuationOrIBSplitSibling(aFrame);
   }
 
   return NS_OK;
 }
 
 static void
 AddFontsFromTextRun(gfxTextRun* aTextRun,
-                    nsIContent* aContent,
+                    nsTextFrame* aFrame,
                     gfxSkipCharsIterator& aSkipIter,
-                    uint32_t aOffset,
-                    uint32_t aLength,
+                    const gfxTextRun::Range& aRange,
                     nsLayoutUtils::UsedFontFaceTable& aFontFaces,
                     uint32_t aMaxRanges)
 {
-  gfxTextRun::Range range(aOffset, aOffset + aLength);
-  gfxTextRun::GlyphRunIterator iter(aTextRun, range);
-  while (iter.NextRun()) {
-    gfxFontEntry *fe = iter.GetGlyphRun()->mFont->GetFontEntry();
+  gfxTextRun::GlyphRunIterator glyphRuns(aTextRun, aRange);
+  nsIContent* content = aFrame->GetContent();
+  int32_t contentLimit = aFrame->GetContentOffset() +
+                         aFrame->GetInFlowContentLength();
+  while (glyphRuns.NextRun()) {
+    gfxFontEntry *fe = glyphRuns.GetGlyphRun()->mFont->GetFontEntry();
     // if we have already listed this face, just make sure the match type is
     // recorded
     InspectorFontFace* fontFace = aFontFaces.Get(fe);
     if (fontFace) {
-      fontFace->AddMatchType(iter.GetGlyphRun()->mMatchType);
+      fontFace->AddMatchType(glyphRuns.GetGlyphRun()->mMatchType);
     } else {
       // A new font entry we haven't seen before
       fontFace = new InspectorFontFace(fe, aTextRun->GetFontGroup(),
-                                       iter.GetGlyphRun()->mMatchType);
+                                       glyphRuns.GetGlyphRun()->mMatchType);
       aFontFaces.Put(fe, fontFace);
     }
+
+    // Add this glyph run to the fontFace's list of ranges, unless we have
+    // already collected as many as wanted.
     if (fontFace->RangeCount() < aMaxRanges) {
-      uint32_t start = aSkipIter.ConvertSkippedToOriginal(iter.GetStringStart());
-      uint32_t end = aSkipIter.ConvertSkippedToOriginal(iter.GetStringEnd());
-      RefPtr<nsRange> range;
-      nsRange::CreateRange(aContent, start, aContent, end, getter_AddRefs(range));
-      fontFace->AddRange(range);
-    }
-  }
-}
-
-/* static */ nsresult
+      int32_t start =
+        aSkipIter.ConvertSkippedToOriginal(glyphRuns.GetStringStart());
+      int32_t end =
+        aSkipIter.ConvertSkippedToOriginal(glyphRuns.GetStringEnd());
+
+      // Mapping back from textrun offsets ("skipped" offsets that reflect the
+      // text after whitespace collapsing, etc) to DOM content offsets in the
+      // original text is ambiguous, because many original characters can
+      // map to a single skipped offset. aSkipIter.ConvertSkippedToOriginal()
+      // will return an "original" offset that corresponds to the *end* of
+      // a collapsed run of characters in this case; but that might extend
+      // beyond the current content node if the textrun mapped multiple nodes.
+      // So we clamp the end offset to keep it valid for the content node
+      // that corresponds to the current textframe.
+      end = std::min(end, contentLimit);
+
+      if (end > start) {
+        RefPtr<nsRange> range;
+        if (NS_FAILED(nsRange::CreateRange(content, start, content, end,
+                                           getter_AddRefs(range)))) {
+          NS_WARNING("failed to create range");
+        } else {
+          fontFace->AddRange(range);
+        }
+      }
+    }
+  }
+}
+
+/* static */ void
 nsLayoutUtils::GetFontFacesForText(nsIFrame* aFrame,
                                    int32_t aStartOffset,
                                    int32_t aEndOffset,
                                    bool aFollowContinuations,
                                    UsedFontFaceTable& aFontFaces,
                                    uint32_t aMaxRanges)
 {
   NS_PRECONDITION(aFrame, "NULL frame pointer");
 
   if (!aFrame->IsTextFrame()) {
-    return NS_OK;
+    return;
+  }
+
+  if (!aFrame->StyleVisibility()->IsVisible()) {
+    return;
   }
 
   nsTextFrame* curr = static_cast<nsTextFrame*>(aFrame);
   do {
     int32_t fstart = std::max(curr->GetContentOffset(), aStartOffset);
     int32_t fend = std::min(curr->GetContentEnd(), aEndOffset);
     if (fstart >= fend) {
       curr = static_cast<nsTextFrame*>(curr->GetNextContinuation());
       continue;
     }
 
     // curr is overlapping with the offset we want
     gfxSkipCharsIterator iter = curr->EnsureTextRun(nsTextFrame::eInflated);
     gfxTextRun* textRun = curr->GetTextRun(nsTextFrame::eInflated);
-    NS_ENSURE_TRUE(textRun, NS_ERROR_OUT_OF_MEMORY);
+    if (!textRun) {
+      NS_WARNING("failed to get textRun, low memory?");
+      return;
+    }
 
     // include continuations in the range that share the same textrun
     nsTextFrame* next = nullptr;
     if (aFollowContinuations && fend < aEndOffset) {
       next = static_cast<nsTextFrame*>(curr->GetNextContinuation());
       while (next && next->GetTextRun(nsTextFrame::eInflated) == textRun) {
         fend = std::min(next->GetContentEnd(), aEndOffset);
         next = fend < aEndOffset ?
           static_cast<nsTextFrame*>(next->GetNextContinuation()) : nullptr;
       }
     }
 
-    uint32_t skipStart = iter.ConvertOriginalToSkipped(fstart);
-    uint32_t skipEnd = iter.ConvertOriginalToSkipped(fend);
-    AddFontsFromTextRun(textRun, aFrame->GetContent(), iter,
-                        skipStart, skipEnd - skipStart, aFontFaces, aMaxRanges);
+    gfxTextRun::Range range(iter.ConvertOriginalToSkipped(fstart),
+                            iter.ConvertOriginalToSkipped(fend));
+    AddFontsFromTextRun(textRun, curr, iter, range, aFontFaces, aMaxRanges);
+
     curr = next;
   } while (aFollowContinuations && curr);
-
-  return NS_OK;
 }
 
 /* static */
 size_t
 nsLayoutUtils::SizeOfTextRunsForFrames(nsIFrame* aFrame,
                                        MallocSizeOf aMallocSizeOf,
                                        bool clear)
 {
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -2289,22 +2289,22 @@ public:
 
   /**
    * Adds all font faces used within the specified range of text in aFrame,
    * and optionally its continuations, to the list in aFontFaceList.
    * Pass 0 and INT32_MAX for aStartOffset and aEndOffset to specify the
    * entire text is to be considered.
    * aMaxRanges: maximum number of text ranges to record for each face.
    */
-  static nsresult GetFontFacesForText(nsIFrame* aFrame,
-                                      int32_t aStartOffset,
-                                      int32_t aEndOffset,
-                                      bool aFollowContinuations,
-                                      UsedFontFaceTable& aResult,
-                                      uint32_t aMaxRanges);
+  static void GetFontFacesForText(nsIFrame* aFrame,
+                                  int32_t aStartOffset,
+                                  int32_t aEndOffset,
+                                  bool aFollowContinuations,
+                                  UsedFontFaceTable& aResult,
+                                  uint32_t aMaxRanges);
 
   /**
    * Walks the frame tree starting at aFrame looking for textRuns.
    * If |clear| is true, just clears the TEXT_RUN_MEMORY_ACCOUNTED flag
    * on each textRun found (and |aMallocSizeOf| is not used).
    * If |clear| is false, adds the storage used for each textRun to the
    * total, and sets the TEXT_RUN_MEMORY_ACCOUNTED flag to avoid double-
    * accounting. (Runs with this flag already set will be skipped.)
--- a/layout/inspector/tests/chrome/test_fontFaceRanges.xul
+++ b/layout/inspector/tests/chrome/test_fontFaceRanges.xul
@@ -163,27 +163,95 @@ function RunTest() {
     default:
       // There shouldn't be any other font used
       ok(false, "unexpected font: " + f.CSSFamilyName);
       break;
     }
   }
   is(familyNames.size, 3, "found all expected families");
 
+  // Testcase involving collapsed whitespace, to test mapping from textrun
+  // offsets back to DOM content offsets.
+  elem = document.getElementById("test5");
+  rng.selectNode(elem);
+  fonts = InspectorUtils.getUsedFontFaces(rng, 10);
+  is(fonts.length, 3, "number of font faces");
+  familyNames.clear();
+  for (var i = 0; i < fonts.length; i++) {
+    var f = fonts[i];
+    familyNames.set(f.CSSFamilyName, true);
+    switch (true) {
+    case f.CSSFamilyName == "capitals":
+      expectRanges(f, ["H"]);
+      break;
+    case f.CSSFamilyName == "lowercase":
+      expectRanges(f, ["ello", "linked", "world"]);
+      break;
+    case f.CSSFamilyName == "gentium":
+      expectRanges(f, ["\n      ", "\n          ", "\n          ", "!\n  "]);
+      break;
+    default:
+      // There shouldn't be any other font used
+      ok(false, "unexpected font: " + f.CSSFamilyName);
+      break;
+    }
+  }
+  is(familyNames.size, 3, "found all expected families");
+
+  // Test that fonts used in non-visible elements are not reported,
+  // nor non-visible ranges for fonts that are also used in visible content.
+  elem = document.getElementById("test6");
+  rng.selectNode(elem);
+  fonts = InspectorUtils.getUsedFontFaces(rng, 10);
+  is(fonts.length, 2, "number of font faces");
+  familyNames.clear();
+  for (var i = 0; i < fonts.length; i++) {
+    var f = fonts[i];
+    familyNames.set(f.CSSFamilyName, true);
+    switch (true) {
+    case f.CSSFamilyName == "capitals":
+      ok(false, "font used in hidden element should not be reported");
+      break;
+    case f.CSSFamilyName == "lowercase":
+      expectRanges(f, ["hello", "visible", "world"]);
+      break;
+    case f.CSSFamilyName == "gentium":
+      expectRanges(f, ["\n      ", "\n      ", "\n      ", "!"]);
+      break;
+    default:
+      // There shouldn't be any other font used
+      ok(false, "unexpected font: " + f.CSSFamilyName);
+      break;
+    }
+  }
+  is(familyNames.size, 2, "found all expected families");
+
   SimpleTest.finish();
 }
 
   ]]>
   </script>
 
   <body xmlns="http://www.w3.org/1999/xhtml">
   <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1435989"
      target="_blank">Mozilla Bug 1435989</a>
   <!-- We use @font-face with unicode-range to force different font faces to be used
        for uppercase, lowercase, and non-letter Latin characters; and then the Chinese
        and Arabic characters will result in font fallback being applied. -->
   <div class="gentium" id="test1">Hello &#x4F60; <b>World</b> &#x597D;!</div>
   <div class="gentium" id="test2">Hello العربي World!</div>
   <div class="gentium" id="test3" style="width:3em">Hello mul&#xAD;ti&#xAD;line World!</div>
   <div class="gentium" id="test4"><span>Hello</span><span>cruel</span>world!</div>
+  <div class="gentium" id="test5">
+      Hello
+          <!-- comment -->
+          <a href="#foo">linked</a>
+          <!-- comment -->
+      world!
+  </div>
+  <div class="gentium" id="test6">
+      hello
+      <a href="#foo" style="visibility:hidden">Non-Visible
+        <span style="visibility:visible">visible</span></a>
+      world!</div>
   </body>
 
 </window>
--- a/layout/reftests/css-disabled/select/reftest.list
+++ b/layout/reftests/css-disabled/select/reftest.list
@@ -1,9 +1,9 @@
 fuzzy-if(Android,12,1) == select-fieldset-1.html select-fieldset-ref.html
-fuzzy-if(skiaContent&&!Android,2,17) == select-fieldset-2.html select-fieldset-ref-disabled.html
+fuzzy-if(Android,12,1) fuzzy-if(skiaContent&&!Android,2,17) == select-fieldset-2.html select-fieldset-ref-disabled.html
 fuzzy-if(skiaContent&&!Android,2,17) == select-fieldset-3.html select-fieldset-ref-disabled.html
-fuzzy-if(skiaContent&&!Android,2,13) == select-fieldset-4.html select-fieldset-ref.html
+fuzzy-if(Android,12,1) fuzzy-if(skiaContent&&!Android,2,13) == select-fieldset-4.html select-fieldset-ref.html
 == select-fieldset-legend-1.html select-fieldset-legend-ref-1.html
-fuzzy-if(skiaContent&&!Android,2,6) == select-fieldset-legend-2.html select-fieldset-legend-ref-2.html
-fuzzy-if(skiaContent&&!Android,2,8) == select-fieldset-legend-3.html select-fieldset-legend-ref-3.html
+fuzzy-if(Android,12,1) fuzzy-if(skiaContent&&!Android,2,6) == select-fieldset-legend-2.html select-fieldset-legend-ref-2.html
+fuzzy-if(Android,12,1) fuzzy-if(skiaContent&&!Android,2,8) == select-fieldset-legend-3.html select-fieldset-legend-ref-3.html
 fuzzy-if(skiaContent,2,12) == select-fieldset-legend-4.html select-fieldset-legend-ref-4.html
 fuzzy-if(skiaContent,2,5) == select-fieldset-legend-5.html select-fieldset-legend-ref-5.html
--- a/modules/libpref/Preferences.cpp
+++ b/modules/libpref/Preferences.cpp
@@ -1101,20 +1101,22 @@ private:
 
   static void HandleError(const char* aMsg)
   {
     nsresult rv;
     nsCOMPtr<nsIConsoleService> console =
       do_GetService("@mozilla.org/consoleservice;1", &rv);
     if (NS_SUCCEEDED(rv)) {
       console->LogStringMessage(NS_ConvertUTF8toUTF16(aMsg).get());
-    } else {
-      printf_stderr("%s\n", aMsg);
     }
-    NS_WARNING(aMsg);
+#ifdef DEBUG
+    NS_ERROR(aMsg);
+#else
+    printf_stderr("%s\n", aMsg);
+#endif
   }
 
   // This is static so that HandlePref() can increment it easily. This is ok
   // because prefs files are read one at a time.
   static uint32_t sNumPrefs;
 };
 
 uint32_t Parser::sNumPrefs = 0;
@@ -1125,39 +1127,39 @@ static void
 TestParseErrorHandlePref(const char* aPrefName,
                          PrefType aType,
                          PrefValueKind aKind,
                          PrefValue aValue,
                          bool aIsSticky)
 {
 }
 
-static char* gTestParseErrorMsg;
+static nsCString gTestParseErrorMsgs;
 
 static void
 TestParseErrorHandleError(const char* aMsg)
 {
-  // aMsg's lifetime is shorter than we need, so duplicate it.
-  gTestParseErrorMsg = moz_xstrdup(aMsg);
+  gTestParseErrorMsgs.Append(aMsg);
+  gTestParseErrorMsgs.Append('\n');
 }
 
 // Keep this in sync with the declaration in test/gtest/Parser.cpp.
 void
 TestParseError(const char* aText, nsCString& aErrorMsg)
 {
   prefs_parser_parse("test",
                      aText,
                      strlen(aText),
                      TestParseErrorHandlePref,
                      TestParseErrorHandleError);
 
-  // Copy the duplicated error message into the outparam, then free it.
-  aErrorMsg.Assign(gTestParseErrorMsg);
-  free(gTestParseErrorMsg);
-  gTestParseErrorMsg = nullptr;
+  // Copy the error messages into the outparam, then clear them from
+  // gTestParseErrorMsgs.
+  aErrorMsg.Assign(gTestParseErrorMsgs);
+  gTestParseErrorMsgs.Truncate();
 }
 
 void
 SendTelemetryLoadData()
 {
   for (auto iter = gTelemetryLoadData->Iter(); !iter.Done(); iter.Next()) {
     const nsCString& filename = PromiseFlatCString(iter.Key());
     const TelemetryLoadData& data = iter.Data();
--- a/modules/libpref/parser/src/lib.rs
+++ b/modules/libpref/parser/src/lib.rs
@@ -36,16 +36,21 @@
 //! - \r\n
 //!
 //! The valid range for <int-value> is -2,147,483,648..2,147,483,647. Values
 //! outside that range will result in a parse error.
 //!
 //! A '\0' char is interpreted as the end of the file. The use of this character
 //! in a prefs file is not recommended. Within string literals \x00 or \u0000
 //! can be used instead.
+//!
+//! The parser performs error recovery. On a syntax error, it will scan forward
+//! to the next ';' token and then continue parsing. If the syntax error occurs
+//! in the middle of a token, it will first finish obtaining the current token
+//! in an appropriate fashion.
 
 // This parser uses several important optimizations.
 //
 // - Because "'\0' means EOF" is part of the grammar (see above) we can match
 //   EOF as a normal char/token, which means we can avoid a typical "do we
 //   still have chars remaining?" test in get_char(), which gives a speedup
 //   because get_char() is a very hot function. (Actually, Rust would
 //   bounds-check this function anyway, so we have get_char_unchecked() which
@@ -112,16 +117,20 @@ type PrefFn = unsafe extern "C" fn(pref_
 /// Keep this in sync with PrefsParserErrorFn in Preferences.cpp.
 type ErrorFn = unsafe extern "C" fn(msg: *const c_char);
 
 /// Parse the contents of a prefs file.
 ///
 /// `buf` is a null-terminated string. `len` is its length, excluding the
 /// null terminator.
 ///
+/// `pref_fn` is called once for each successfully parsed pref.
+///
+/// `error_fn` is called once for each parse error detected.
+///
 /// Keep this in sync with the prefs_parser_parse() declaration in
 /// Preferences.cpp.
 #[no_mangle]
 pub extern "C" fn prefs_parser_parse(path: *const c_char, buf: *const c_char, len: usize,
                                      pref_fn: PrefFn, error_fn: ErrorFn) -> bool {
     let path = unsafe { std::ffi::CStr::from_ptr(path).to_string_lossy().into_owned() };
 
     // Make sure `buf` ends in a '\0', and include that in the length, because
@@ -157,16 +166,23 @@ enum Token {
     // tokens, so these token values are always positive. Furthermore, we
     // tokenize int literals as u32 so that 2147483648 (which doesn't fit into
     // an i32) can be subsequently negated to -2147483648 (which does fit into
     // an i32) if a '-' token precedes it.
     Int(u32),
 
     // Malformed token.
     Error(&'static str),
+
+    // Malformed token at a particular line number. For use when
+    // Parser::line_num might not be the right line number when the error is
+    // reported. E.g. if a multi-line string has a bad escape sequence on the
+    // first line, we don't report the error until the string's end has been
+    // reached.
+    ErrorAtLine(&'static str, u32),
 }
 
 // We categorize every char by what action should be taken when it appears at
 // the start of a new token.
 #[derive(Clone, Copy, PartialEq)]
 enum CharKind {
     // These are ordered by frequency. See the comment in GetToken().
     SingleChar, // Unambiguous single-char tokens: [()+,-]
@@ -267,16 +283,17 @@ const KEYWORD_INFOS: &[KeywordInfo; 5] =
 
 struct Parser<'t> {
     path: &'t str,      // Path to the file being parsed. Used in error messages.
     buf: &'t [u8],      // Text being parsed.
     i: usize,           // Index of next char to be read.
     line_num: u32,      // Current line number within the text.
     pref_fn: PrefFn,    // Callback for processing each pref.
     error_fn: ErrorFn,  // Callback for parse errors.
+    has_errors: bool,   // Have we encountered errors?
 }
 
 // As described above, we use 0 to represent EOF.
 const EOF: u8 = b'\0';
 
 impl<'t> Parser<'t> {
     fn new(path: &'t str, buf: &'t [u8], pref_fn: PrefFn, error_fn: ErrorFn) -> Parser<'t> {
         // Make sure these tables take up 1 byte per entry.
@@ -285,74 +302,69 @@ impl<'t> Parser<'t> {
 
         Parser {
             path: path,
             buf: buf,
             i: 0,
             line_num: 1,
             pref_fn: pref_fn,
             error_fn: error_fn,
+            has_errors: false,
         }
     }
 
     fn parse(&mut self) -> bool {
         // These are reused, because allocating a new Vec for every string is slow.
         let mut name_str  = Vec::with_capacity(128); // For pref names.
         let mut value_str = Vec::with_capacity(512); // For string pref values.
         let mut none_str  = Vec::with_capacity(0);   // For tokens that shouldn't be strings.
 
-        loop {
-            // Note: if you add error recovery here, be aware that the
-            // erroneous char may have been the text-ending EOF, in which case
-            // self.i will point one past the end of the text. You should check
-            // for that possibility before getting more chars.
+        let mut token = self.get_token(&mut none_str);
 
-            // EOF?
-            let token = self.get_token(&mut none_str);
-            if token == Token::SingleChar(EOF) {
-                break;
-            }
-
+        // At the top of the loop we already have a token. In a valid input
+        // this will be either the first token of a new pref, or EOF.
+        loop {
             // <pref-spec>
             let (pref_value_kind, is_sticky) = match token {
-                Token::Pref => {
-                    (PrefValueKind::Default, false)
-                }
-                Token::StickyPref => {
-                    (PrefValueKind::Default, true)
+                Token::Pref => (PrefValueKind::Default, false),
+                Token::StickyPref => (PrefValueKind::Default, true),
+                Token::UserPref => (PrefValueKind::User, false),
+                Token::SingleChar(EOF) => return !self.has_errors,
+                _ => {
+                    token = self.error_and_recover(
+                        token, "expected pref specifier at start of pref definition");
+                    continue;
                 }
-                Token::UserPref => {
-                    (PrefValueKind::User, false)
-                }
-                _ => return self.error(token,
-                                       "expected pref specifier at start of pref definition")
             };
 
             // "("
-            let token = self.get_token(&mut none_str);
+            token = self.get_token(&mut none_str);
             if token != Token::SingleChar(b'(') {
-                return self.error(token, "expected '(' after pref specifier");
+                token = self.error_and_recover(token, "expected '(' after pref specifier");
+                continue;
             }
 
             // <pref-name>
-            let token = self.get_token(&mut name_str);
+            token = self.get_token(&mut name_str);
             let pref_name = if token == Token::String {
                 &name_str
             } else {
-                return self.error(token, "expected pref name after '('");
+                token = self.error_and_recover(token, "expected pref name after '('");
+                continue;
             };
 
             // ","
-            let token = self.get_token(&mut none_str);
+            token = self.get_token(&mut none_str);
             if token != Token::SingleChar(b',') {
-                return self.error(token, "expected ',' after pref name");
+                token = self.error_and_recover(token, "expected ',' after pref name");
+                continue;
             }
 
             // <pref-value>
-            let token = self.get_token(&mut value_str);
+            token = self.get_token(&mut value_str);
             let (pref_type, pref_value) = match token {
                 Token::True => {
                     (PrefType::Bool, PrefValue { bool_val: true })
                 }
                 Token::False => {
                     (PrefType::Bool, PrefValue { bool_val: false })
                 }
                 Token::String => {
@@ -360,99 +372,134 @@ impl<'t> Parser<'t> {
                      PrefValue { string_val: value_str.as_ptr() as *const c_char })
 
                 }
                 Token::Int(u) => {
                     // Accept u <= 2147483647; anything larger will overflow i32.
                     if u <= std::i32::MAX as u32 {
                         (PrefType::Int, PrefValue { int_val: u as i32 })
                     } else {
-                        return self.error(Token::Error("integer literal overflowed"), "");
+                        token = self.error_and_recover(
+                            Token::Error("integer literal overflowed"), "");
+                        continue;
                     }
-
                 }
                 Token::SingleChar(b'-') => {
-                    let token = self.get_token(&mut none_str);
+                    token = self.get_token(&mut none_str);
                     if let Token::Int(u) = token {
                         // Accept u <= 2147483648; anything larger will overflow i32 once negated.
                         if u <= std::i32::MAX as u32 {
                             (PrefType::Int, PrefValue { int_val: -(u as i32) })
                         } else if u == std::i32::MAX as u32 + 1 {
                             (PrefType::Int, PrefValue { int_val: std::i32::MIN })
                         } else {
-                            return self.error(Token::Error("integer literal overflowed"), "");
+                            token = self.error_and_recover(
+                                Token::Error("integer literal overflowed"), "");
+                            continue;
                         }
                     } else {
-                        return self.error(token, "expected integer literal after '-'");
+                        token = self.error_and_recover(
+                            token, "expected integer literal after '-'");
+                        continue;
                     }
 
                 }
                 Token::SingleChar(b'+') => {
-                    let token = self.get_token(&mut none_str);
+                    token = self.get_token(&mut none_str);
                     if let Token::Int(u) = token {
                         // Accept u <= 2147483647; anything larger will overflow i32.
                         if u <= std::i32::MAX as u32 {
                             (PrefType::Int, PrefValue { int_val: u as i32 })
                         } else {
-                            return self.error(Token::Error("integer literal overflowed"), "");
+                            token = self.error_and_recover(
+                                Token::Error("integer literal overflowed"), "");
+                            continue;
                         }
                     } else {
-                        return self.error(token, "expected integer literal after '+'");
+                        token = self.error_and_recover(token, "expected integer literal after '+'");
+                        continue;
                     }
 
                 }
-                _ => return self.error(token, "expected pref value after ','")
+                _ => {
+                    token = self.error_and_recover(token, "expected pref value after ','");
+                    continue;
+                }
             };
 
             // ")"
-            let token = self.get_token(&mut none_str);
+            token = self.get_token(&mut none_str);
             if token != Token::SingleChar(b')') {
-                return self.error(token, "expected ')' after pref value");
+                token = self.error_and_recover(token, "expected ')' after pref value");
+                continue;
             }
 
             // ";"
-            let token = self.get_token(&mut none_str);
+            token = self.get_token(&mut none_str);
             if token != Token::SingleChar(b';') {
-                return self.error(token, "expected ';' after ')'");
+                token = self.error_and_recover(token, "expected ';' after ')'");
+                continue;
             }
 
             unsafe { (self.pref_fn)(pref_name.as_ptr() as *const c_char, pref_type, pref_value_kind,
                                     pref_value, is_sticky) };
+
+            token = self.get_token(&mut none_str);
         }
-
-        true
     }
 
-    fn error(&self, token: Token, msg: &str) -> bool {
-        // If `token` is a Token::Error, it's a lexing error and the error
-        // message is within `token`. Otherwise, it's a parsing error and the
-        // error message is in `msg`.
-        let msg = if let Token::Error(token_msg) = token {
-            token_msg
-        } else {
-            msg
+    fn error_and_recover(&mut self, token: Token, msg: &str) -> Token {
+        self.has_errors = true;
+
+        // If `token` is a Token::{Error,ErrorAtLine}, it's a lexing error and
+        // the error message is within `token`. Otherwise, it's a parsing error
+        // and the error message is in `msg`.
+        let (msg, line_num) = match token {
+            Token::Error(token_msg) => (token_msg, self.line_num),
+            Token::ErrorAtLine(token_msg, line_num) => (token_msg, line_num),
+            _ => (msg, self.line_num),
         };
-        let msg = format!("{}:{}: prefs parse error: {}", self.path, self.line_num, msg);
+        let msg = format!("{}:{}: prefs parse error: {}", self.path, line_num, msg);
         let msg = std::ffi::CString::new(msg).unwrap();
         unsafe { (self.error_fn)(msg.as_ptr() as *const c_char) };
 
-        false
+        // "Panic-mode" recovery: consume tokens until one of the following
+        // occurs.
+        // - We hit a semicolon, whereupon we return the following token.
+        // - We hit EOF, whereupon we return EOF.
+        //
+        // For this to work, if the lexing functions hit EOF in an error case
+        // they must unget it so we can safely reget it here.
+        //
+        // If the starting token (passed in above) is EOF we must not get
+        // another token otherwise we will read past the end of `self.buf`.
+        let mut dummy_str = Vec::with_capacity(128);
+        let mut token = token;
+        loop {
+            match token {
+                Token::SingleChar(b';') => return self.get_token(&mut dummy_str),
+                Token::SingleChar(EOF) => return token,
+                _ => {}
+            }
+            token = self.get_token(&mut dummy_str);
+        }
     }
 
     #[inline(always)]
     fn get_char(&mut self) -> u8 {
         let c = self.buf[self.i];
         self.i += 1;
         c
     }
 
-    // This function skips the bounds check. Using it at the hottest two call
-    // sites gives a ~15% parsing speed boost.
+    // This function skips the bounds check in non-optimized builds. Using it
+    // at the hottest two call sites gives a ~15% parsing speed boost.
     #[inline(always)]
     unsafe fn get_char_unchecked(&mut self) -> u8 {
+        debug_assert!(self.i < self.buf.len());
         let c = *self.buf.get_unchecked(self.i);
         self.i += 1;
         c
     }
 
     #[inline(always)]
     fn unget_char(&mut self) {
         debug_assert!(self.i > 0);
@@ -486,19 +533,17 @@ impl<'t> Parser<'t> {
                     break;
                 }
                 b'\r' => {
                     self.line_num += 1;
                     self.match_char(b'\n');
                     break;
                 }
                 EOF => {
-                    // We must unget the EOF otherwise we'll read past it the
-                    // next time around the main loop in get_token(), violating
-                    // self.buf's bounds.
+                    // Unget EOF so subsequent calls to get_char() are safe.
                     self.unget_char();
                     break;
                 }
                 _ => continue
             }
         }
     }
 
@@ -515,33 +560,39 @@ impl<'t> Parser<'t> {
                 b'\n' => {
                     self.line_num += 1;
                 }
                 b'\r' => {
                     self.line_num += 1;
                     self.match_char(b'\n');
                 }
                 EOF => {
+                    // Unget EOF so subsequent calls to get_char() are safe.
+                    self.unget_char();
                     return false
                 }
                 _ => continue
             }
         }
     }
 
     fn match_hex_digits(&mut self, ndigits: i32) -> Option<u16> {
         debug_assert!(ndigits == 2 || ndigits == 4);
         let mut value: u16 = 0;
         for _ in 0..ndigits {
             value = value << 4;
             match self.get_char() {
                 c @ b'0'... b'9' => value += (c - b'0') as u16,
                 c @ b'A'...b'F' => value += (c - b'A') as u16 + 10,
                 c @ b'a'...b'f' => value += (c - b'a') as u16 + 10,
-                _ => return None
+                _ => {
+                    // Unget in case the char was a closing quote or EOF.
+                    self.unget_char();
+                    return None;
+                }
             }
         }
         Some(value)
     }
 
     #[inline(always)]
     fn char_kind(c: u8) -> CharKind {
         // Use get_unchecked() because a u8 index cannot exceed this table's
@@ -615,57 +666,65 @@ impl<'t> Parser<'t> {
                                 return Token::Error("unterminated /* comment");
                             }
                         }
                         _ => return Token::Error("expected '/' or '*' after '/'")
                     }
                     continue;
                 }
                 CharKind::Digit => {
-                    let mut value = (c - b'0') as u32;
+                    let mut value = Some((c - b'0') as u32);
                     loop {
                         let c = self.get_char();
                         match Parser::char_kind(c) {
                             CharKind::Digit => {
-                                fn add_digit(v: u32, c: u8) -> Option<u32> {
-                                    v.checked_mul(10)?.checked_add((c - b'0') as u32)
+                                fn add_digit(value: Option<u32>, c: u8) -> Option<u32> {
+                                    value?.checked_mul(10)?.checked_add((c - b'0') as u32)
                                 }
-                                if let Some(v) = add_digit(value, c) {
-                                    value = v;
-                                } else {
-                                    return Token::Error("integer literal overflowed");
-                                }
+                                value = add_digit(value, c);
                             }
                             CharKind::Keyword => {
-                                // Reject things like "123foo".
-                                return Token::Error(
-                                    "unexpected character in integer literal");
+                                // Reject things like "123foo". Error recovery
+                                // will retokenize from "foo" onward.
+                                self.unget_char();
+                                return Token::Error("unexpected character in integer literal");
                             }
                             _ => {
                                 self.unget_char();
                                 break;
                             }
                         }
                     }
-                    return Token::Int(value);
+                    return match value {
+                        Some(v) => Token::Int(v),
+                        None => Token::Error("integer literal overflowed"),
+                    };
                 }
                 CharKind::Hash => {
                     self.match_single_line_comment();
                     continue;
                 }
                 CharKind::CR => {
                     self.match_char(b'\n');
                     self.line_num += 1;
                     continue;
                 }
+                // Error recovery will retokenize from the next character.
                 _ => return Token::Error("unexpected character")
             }
         }
     }
 
+    fn string_error_token(&self, token: &mut Token, msg: &'static str) {
+        // We only want to capture the first tokenization error within a string.
+        if *token == Token::String {
+            *token = Token::ErrorAtLine(msg, self.line_num);
+        }
+    }
+
     // Always inline this because it has a single call site.
     #[inline(always)]
     fn get_string_token(&mut self, quote_char: u8, str_buf: &mut Vec<u8>) -> Token {
         // First scan through the string to see if it contains any chars that
         // need special handling.
         let start = self.i;
         let has_special_chars = loop {
             // To reach here, the previous char must have been a quote
@@ -685,17 +744,22 @@ impl<'t> Parser<'t> {
         if !has_special_chars {
           str_buf.extend(&self.buf[start..self.i - 1]);
           str_buf.push(b'\0');
           return Token::String;
         }
 
         // There were special chars. Re-scan the string, filling in str_buf one
         // char at a time.
+        //
+        // On error, we change `token` to an error token and then keep going to
+        // the end of the string literal. `str_buf` won't be used in that case.
         self.i = start;
+        let mut token = Token::String;
+
         loop {
             let c = self.get_char();
             let c2 = if !Parser::is_special_string_char(c) {
                 c
 
             } else if c == quote_char {
                 break;
 
@@ -707,76 +771,93 @@ impl<'t> Parser<'t> {
                     b'n'  => b'\n',
                     b'r'  => b'\r',
                     b'x'  => {
                         if let Some(value) = self.match_hex_digits(2) {
                             debug_assert!(value <= 0xff);
                             if value != 0 {
                                 value as u8
                             } else {
-                                return Token::Error("\\x00 is not allowed");
+                                self.string_error_token(&mut token, "\\x00 is not allowed");
+                                continue;
                             }
                         } else {
-                            return Token::Error("malformed \\x escape sequence");
+                            self.string_error_token(&mut token, "malformed \\x escape sequence");
+                            continue;
                         }
                     }
                     b'u' => {
                         if let Some(value) = self.match_hex_digits(4) {
                             let mut utf16 = vec![value];
                             if 0xd800 == (0xfc00 & value) {
                                 // High surrogate value. Look for the low surrogate value.
                                 if self.match_char(b'\\') && self.match_char(b'u') {
                                     if let Some(lo) = self.match_hex_digits(4) {
                                         if 0xdc00 == (0xfc00 & lo) {
                                             // Found a valid low surrogate.
                                             utf16.push(lo);
                                         } else {
-                                            return Token::Error(
+                                            self.string_error_token(
+                                                &mut token,
                                                 "invalid low surrogate value after high surrogate");
+                                            continue;
                                         }
                                     }
                                 }
                                 if utf16.len() != 2 {
-                                    return Token::Error(
-                                        "expected low surrogate after high surrogate");
+                                    self.string_error_token(
+                                        &mut token, "expected low surrogate after high surrogate");
+                                    continue;
                                 }
                             } else if value == 0 {
-                                return Token::Error("\\u0000 is not allowed");
+                                self.string_error_token(&mut token, "\\u0000 is not allowed");
+                                continue;
                             }
 
                             // Insert the UTF-16 sequence as UTF-8.
                             let utf8 = String::from_utf16(&utf16).unwrap();
                             str_buf.extend(utf8.as_bytes());
                         } else {
-                            return Token::Error("malformed \\u escape sequence");
+                            self.string_error_token(&mut token, "malformed \\u escape sequence");
+                            continue;
                         }
                         continue; // We don't want to str_buf.push(c2) below.
                     }
-                    _ => return Token::Error("unexpected escape sequence character after '\\'")
+                    _ => {
+                        // Unget in case the char is an EOF.
+                        self.unget_char();
+                        self.string_error_token(
+                            &mut token, "unexpected escape sequence character after '\\'");
+                        continue;
+                    }
                 }
 
             } else if c == b'\n' {
                 self.line_num += 1;
                 c
 
             } else if c == b'\r' {
                 self.line_num += 1;
                 if self.match_char(b'\n') {
                     str_buf.push(b'\r');
                     b'\n'
                 } else {
                     c
                 }
 
             } else if c == EOF {
-                return Token::Error("unterminated string literal");
+                // Unget EOF so subsequent calls to get_char() are safe.
+                self.unget_char();
+                self.string_error_token(&mut token, "unterminated string literal");
+                break;
 
             } else {
                 // This case is only hit for the non-closing quote char.
                 debug_assert!((c == b'\'' || c == b'\"') && c != quote_char);
                 c
             };
             str_buf.push(c2);
         }
         str_buf.push(b'\0');
-        return Token::String;
+
+        token
     }
 }
--- a/modules/libpref/test/gtest/Parser.cpp
+++ b/modules/libpref/test/gtest/Parser.cpp
@@ -23,503 +23,414 @@ TEST(PrefsParser, Errors)
   do {                                                                         \
     TestParseError(text_, actualErrorMsg);                                     \
     ASSERT_STREQ(expectedErrorMsg_, actualErrorMsg.get());                     \
   } while (0)
 
   // clang-format off
 
   //-------------------------------------------------------------------------
-  // Valid syntax, just as a sanity test. (More thorough testing of valid syntax
-  // and semantics is done in modules/libpref/test/unit/test_parser.js.)
+  // Valid syntax. (Other testing of more typical valid syntax and semantics is
+  // done in modules/libpref/test/unit/test_parser.js.)
   //-------------------------------------------------------------------------
 
+  // Normal prefs.
   P(R"(
 pref("bool", true);
 sticky_pref("int", 123);
 user_pref("string", "value");
     )",
     ""
   );
 
+  // Totally empty input.
+  P("",
+    ""
+  );
+
+  // Whitespace-only input.
+  P(R"(   
+		
+    )" "\v \t \v \f",
+    ""
+  );
+
   //-------------------------------------------------------------------------
   // All the lexing errors. (To be pedantic, some of the integer literal
   // overflows are triggered in the parser, but put them all here so they're all
   // in the one spot.)
   //-------------------------------------------------------------------------
 
   // Integer overflow errors.
-
   P(R"(
 pref("int.ok", 2147483647);
 pref("int.overflow", 2147483648);
-    )",
-    "test:3: prefs parse error: integer literal overflowed");
-
-  P(R"(
 pref("int.ok", +2147483647);
 pref("int.overflow", +2147483648);
-    )",
-    "test:3: prefs parse error: integer literal overflowed"
-  );
-
-  P(R"(
 pref("int.ok", -2147483648);
 pref("int.overflow", -2147483649);
-    )",
-    "test:3: prefs parse error: integer literal overflowed"
-  );
-
-  P(R"(
 pref("int.overflow", 4294967296);
-    )",
-    "test:2: prefs parse error: integer literal overflowed"
-  );
-
-  P(R"(
 pref("int.overflow", +4294967296);
-    )",
-    "test:2: prefs parse error: integer literal overflowed"
-  );
-
-  P(R"(
 pref("int.overflow", -4294967296);
-    )",
-    "test:2: prefs parse error: integer literal overflowed"
-  );
-
-  P(R"(
 pref("int.overflow", 4294967297);
-    )",
-    "test:2: prefs parse error: integer literal overflowed"
-  );
-
-  P(R"(
 pref("int.overflow", 1234567890987654321);
     )",
-    "test:2: prefs parse error: integer literal overflowed"
+    "test:3: prefs parse error: integer literal overflowed\n"
+    "test:5: prefs parse error: integer literal overflowed\n"
+    "test:7: prefs parse error: integer literal overflowed\n"
+    "test:8: prefs parse error: integer literal overflowed\n"
+    "test:9: prefs parse error: integer literal overflowed\n"
+    "test:10: prefs parse error: integer literal overflowed\n"
+    "test:11: prefs parse error: integer literal overflowed\n"
+    "test:12: prefs parse error: integer literal overflowed\n"
   );
 
   // Other integer errors.
-
   P(R"(
 pref("int.unexpected", 100foo);
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: unexpected character in integer literal"
+    "test:2: prefs parse error: unexpected character in integer literal\n"
   );
 
-  // \x escape errors.
-
   // \x00 is not allowed.
   P(R"(
 pref("string.bad-x-escape", "foo\x00bar");
-    )",
-    "test:2: prefs parse error: \\x00 is not allowed"
-  );
-
-  // End of string after \x.
-  P(R"(
-pref("string.bad-x-escape", "foo\x");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: malformed \\x escape sequence"
-  );
-
-  // Punctuation after \x.
-  P(R"(
-pref("string.bad-x-escape", "foo\x,bar");
-    )",
-    "test:2: prefs parse error: malformed \\x escape sequence"
+    "test:2: prefs parse error: \\x00 is not allowed\n"
   );
 
-  // Space after \x.
+  // Various bad things after \x: end of string, punctuation, space, newline,
+  // EOF.
   P(R"(
+pref("string.bad-x-escape", "foo\x");
+pref("string.bad-x-escape", "foo\x,bar");
 pref("string.bad-x-escape", "foo\x 12");
-    )",
-    "test:2: prefs parse error: malformed \\x escape sequence"
-  );
-
-  // Newline after \x.
-  P(R"(
 pref("string.bad-x-escape", "foo\x
 12");
-    )",
-    "test:2: prefs parse error: malformed \\x escape sequence"
-  );
-
-  // EOF after \x.
-  P(R"(
 pref("string.bad-x-escape", "foo\x)",
-    "test:2: prefs parse error: malformed \\x escape sequence"
+    "test:2: prefs parse error: malformed \\x escape sequence\n"
+    "test:3: prefs parse error: malformed \\x escape sequence\n"
+    "test:4: prefs parse error: malformed \\x escape sequence\n"
+    "test:5: prefs parse error: malformed \\x escape sequence\n"
+    "test:7: prefs parse error: malformed \\x escape sequence\n"
   );
 
   // Not enough hex digits.
   P(R"(
 pref("string.bad-x-escape", "foo\x1");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: malformed \\x escape sequence"
+    "test:2: prefs parse error: malformed \\x escape sequence\n"
   );
 
   // Invalid hex digit.
   P(R"(
 pref("string.bad-x-escape", "foo\x1G");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: malformed \\x escape sequence"
+    "test:2: prefs parse error: malformed \\x escape sequence\n"
   );
 
-  // \u escape errors.
-
   // \u0000 is not allowed.
   // (The string literal is broken in two so that MSVC doesn't complain about
   // an invalid universal-character-name.)
   P(R"(
 pref("string.bad-u-escape", "foo\)" R"(u0000 bar");
-    )",
-    "test:2: prefs parse error: \\u0000 is not allowed"
-  );
-
-  // End of string after \u.
-  P(R"(
-pref("string.bad-u-escape", "foo\u");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
-  );
-
-  // Punctuation after \u.
-  P(R"(
-pref("string.bad-u-escape", "foo\u,bar");
-    )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
+    "test:2: prefs parse error: \\u0000 is not allowed\n"
   );
 
-  // Space after \u.
+  // Various bad things after \u: end of string, punctuation, space, newline,
+  // EOF.
   P(R"(
+pref("string.bad-u-escape", "foo\u");
+pref("string.bad-u-escape", "foo\u,bar");
 pref("string.bad-u-escape", "foo\u 1234");
-    )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
-  );
-
-  // Newline after \u.
-  P(R"(
 pref("string.bad-u-escape", "foo\u
 1234");
-    )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
-  );
-
-  // EOF after \u.
-  P(R"(
 pref("string.bad-u-escape", "foo\u)",
-    "test:2: prefs parse error: malformed \\u escape sequence"
+    "test:2: prefs parse error: malformed \\u escape sequence\n"
+    "test:3: prefs parse error: malformed \\u escape sequence\n"
+    "test:4: prefs parse error: malformed \\u escape sequence\n"
+    "test:5: prefs parse error: malformed \\u escape sequence\n"
+    "test:7: prefs parse error: malformed \\u escape sequence\n"
   );
 
   // Not enough hex digits.
   P(R"(
 pref("string.bad-u-escape", "foo\u1");
-    )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
-  );
-
-  // Not enough hex digits.
-  P(R"(
 pref("string.bad-u-escape", "foo\u12");
+pref("string.bad-u-escape", "foo\u123");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
-  );
-
-  // Not enough hex digits.
-  P(R"(
-pref("string.bad-u-escape", "foo\u123");
-    )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
+    "test:2: prefs parse error: malformed \\u escape sequence\n"
+    "test:3: prefs parse error: malformed \\u escape sequence\n"
+    "test:4: prefs parse error: malformed \\u escape sequence\n"
   );
 
   // Invalid hex digit.
   P(R"(
 pref("string.bad-u-escape", "foo\u1G34");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: malformed \\u escape sequence"
+    "test:2: prefs parse error: malformed \\u escape sequence\n"
   );
 
   // High surrogate not followed by low surrogate.
   // (The string literal is broken in two so that MSVC doesn't complain about
   // an invalid universal-character-name.)
   P(R"(
 pref("string.bad-u-surrogate", "foo\)" R"(ud83c,blah");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: expected low surrogate after high surrogate"
+    "test:2: prefs parse error: expected low surrogate after high surrogate\n"
   );
 
   // High surrogate followed by invalid low surrogate value.
   // (The string literal is broken in two so that MSVC doesn't complain about
   // an invalid universal-character-name.)
   P(R"(
 pref("string.bad-u-surrogate", "foo\)" R"(ud83c\u1234");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: invalid low surrogate value after high surrogate"
-  );
-
-  // Bad escape characters.
-
-  // Unlike in JavaScript, \b isn't allowed.
-  P(R"(
-pref("string.bad-escape", "foo\v");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
+    "test:2: prefs parse error: invalid low surrogate value after high surrogate\n"
   );
 
-  // Unlike in JavaScript, \f isn't allowed.
+  // Unlike in JavaScript, \b, \f, \t, \v aren't allowed.
   P(R"(
+pref("string.bad-escape", "foo\b");
 pref("string.bad-escape", "foo\f");
+pref("string.bad-escape", "foo\t");
+pref("string.bad-escape", "foo\v");
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
+    "test:2: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:3: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:4: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:5: prefs parse error: unexpected escape sequence character after '\\'\n"
   );
 
-  // Unlike in JavaScript, \t isn't allowed.
-  P(R"(
-pref("string.bad-escape", "foo\t");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
-  );
-
-  // Unlike in JavaScript, \v isn't allowed.
-  P(R"(
-pref("string.bad-escape", "foo\v");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
-  );
-
-  // Non-special letter after \.
+  // Various bad things after \: non-special letter, number, punctuation,
+  // space, newline, EOF.
   P(R"(
 pref("string.bad-escape", "foo\Q");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
-  );
-
-  // Number after \.
-  P(R"(
 pref("string.bad-escape", "foo\1");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
-  );
-
-  // Punctuation after \.
-  P(R"(
 pref("string.bad-escape", "foo\,");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
-  );
-
-  // Space after \.
-  P(R"(
 pref("string.bad-escape", "foo\ n");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
-  );
-
-  // Newline after \.
-  P(R"(
 pref("string.bad-escape", "foo\
 n");
-    )",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
-  );
-
-  // EOF after \.
-  P(R"(
 pref("string.bad-escape", "foo\)",
-    "test:2: prefs parse error: unexpected escape sequence character after '\\'"
+    "test:2: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:3: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:4: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:5: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:6: prefs parse error: unexpected escape sequence character after '\\'\n"
+    "test:8: prefs parse error: unexpected escape sequence character after '\\'\n"
   );
 
   // Unterminated string literals.
 
   // Simple case.
   P(R"(
 pref("string.unterminated-string", "foo
     )",
-    "test:3: prefs parse error: unterminated string literal"
+    "test:3: prefs parse error: unterminated string literal\n"
+  );
+
+  // Alternative case; `int` comes after the string and is seen as a keyword.
+  // The parser then skips to the ';', so no error about the unterminated
+  // string is issued.
+  P(R"(
+pref("string.unterminated-string", "foo);
+pref("int.ok", 0);
+    )",
+    "test:3: prefs parse error: unknown keyword\n"
   );
 
   // Mismatched quotes (1).
   P(R"(
 pref("string.unterminated-string", "foo');
     )",
-    "test:3: prefs parse error: unterminated string literal"
+    "test:3: prefs parse error: unterminated string literal\n"
   );
 
   // Mismatched quotes (2).
   P(R"(
 pref("string.unterminated-string", 'foo");
     )",
-    "test:3: prefs parse error: unterminated string literal"
-  );
-
-  // Unknown keywords
-
-  P(R"(
-foo
-    )",
-    "test:2: prefs parse error: unknown keyword"
-  );
-
-  P(R"(
-preff("string.bad-keyword", true);
-    )",
-    "test:2: prefs parse error: unknown keyword"
+    "test:3: prefs parse error: unterminated string literal\n"
   );
 
+  // Unknown keywords.
   P(R"(
+foo;
+preff("string.bad-keyword", true);
 ticky_pref("string.bad-keyword", true);
+User_pref("string.bad-keyword", true);
+pref("string.bad-keyword", TRUE);
     )",
-    "test:2: prefs parse error: unknown keyword"
+    "test:2: prefs parse error: unknown keyword\n"
+    "test:3: prefs parse error: unknown keyword\n"
+    "test:4: prefs parse error: unknown keyword\n"
+    "test:5: prefs parse error: unknown keyword\n"
+    "test:6: prefs parse error: unknown keyword\n"
   );
 
-  P(R"(
-User_pref("string.bad-keyword", true);
-    )",
-    "test:2: prefs parse error: unknown keyword"
-  );
-
-  P(R"(
-pref("string.bad-keyword", TRUE);
-    )",
-    "test:2: prefs parse error: unknown keyword"
-  );
-
-  // Unterminated C-style comment
+  // Unterminated C-style comment.
   P(R"(
 /* comment
     )",
-    "test:3: prefs parse error: unterminated /* comment"
+    "test:3: prefs parse error: unterminated /* comment\n"
   );
 
-  // Malformed comments.
-
+  // Malformed comment.
   P(R"(
 / comment
     )",
-    "test:2: prefs parse error: expected '/' or '*' after '/'"
+    "test:2: prefs parse error: expected '/' or '*' after '/'\n"
   );
 
-  // Unexpected characters
-
+  // C++-style comment ending in EOF (1).
   P(R"(
-pref("unexpected.chars", &true);
-    )",
-    "test:2: prefs parse error: unexpected character"
+// comment)",
+    ""
   );
 
+  // C++-style comment ending in EOF (2).
   P(R"(
-pref("unexpected.chars" : true);
-    )",
-    "test:2: prefs parse error: unexpected character"
+//)",
+    ""
   );
 
+  // Various unexpected characters.
   P(R"(
+pref("unexpected.chars", &true);
+pref("unexpected.chars" : true);
 @pref("unexpected.chars", true);
-    )",
-    "test:2: prefs parse error: unexpected character"
-  );
-
-  P(R"(
 pref["unexpected.chars": true];
     )",
-    "test:2: prefs parse error: unexpected character"
+    "test:2: prefs parse error: unexpected character\n"
+    "test:3: prefs parse error: unexpected character\n"
+    "test:4: prefs parse error: unexpected character\n"
+    "test:5: prefs parse error: unexpected character\n"
   );
 
   //-------------------------------------------------------------------------
   // All the parsing errors.
   //-------------------------------------------------------------------------
 
   P(R"(
 "pref"("parse.error": true);
-    )",
-    "test:2: prefs parse error: expected pref specifier at start of pref definition"
-  );
-
-  P(R"(
 pref1("parse.error": true);
-    )",
-    "test:2: prefs parse error: expected '(' after pref specifier"
+pref(123: true);
+pref("parse.error" true);
+pref("parse.error", pref);
+pref("parse.error", -true);
+pref("parse.error", +"value");
+pref("parse.error", true;
+pref("parse.error", true)
+pref("int.ok", 1);
+pref("parse.error", true))",
+    "test:2: prefs parse error: expected pref specifier at start of pref definition\n"
+    "test:3: prefs parse error: expected '(' after pref specifier\n"
+    "test:4: prefs parse error: expected pref name after '('\n"
+    "test:5: prefs parse error: expected ',' after pref name\n"
+    "test:6: prefs parse error: expected pref value after ','\n"
+    "test:7: prefs parse error: expected integer literal after '-'\n"
+    "test:8: prefs parse error: expected integer literal after '+'\n"
+    "test:9: prefs parse error: expected ')' after pref value\n"
+    "test:11: prefs parse error: expected ';' after ')'\n"
+    "test:12: prefs parse error: expected ';' after ')'\n"
   );
 
-  P(R"(
-pref(123: true);
-    )",
-    "test:2: prefs parse error: expected pref name after '('"
-  );
+  // Parse errors involving unexpected EOF.
 
   P(R"(
-pref("parse.error" true);
-    )",
-    "test:2: prefs parse error: expected ',' after pref name"
+pref)",
+    "test:2: prefs parse error: expected '(' after pref specifier\n"
   );
 
   P(R"(
-pref("parse.error", -true);
-    )",
-    "test:2: prefs parse error: expected integer literal after '-'"
+pref()",
+    "test:2: prefs parse error: expected pref name after '('\n"
   );
 
   P(R"(
-pref("parse.error", +"value");
-    )",
-    "test:2: prefs parse error: expected integer literal after '+'"
+pref("parse.error")",
+    "test:2: prefs parse error: expected ',' after pref name\n"
+  );
+
+  P(R"(
+pref("parse.error",)",
+    "test:2: prefs parse error: expected pref value after ','\n"
   );
 
   P(R"(
-pref("parse.error", pref);
-    )",
-    "test:2: prefs parse error: expected pref value after ','"
+pref("parse.error", -)",
+    "test:2: prefs parse error: expected integer literal after '-'\n"
   );
 
   P(R"(
-pref("parse.error", true;
-    )",
-    "test:2: prefs parse error: expected ')' after pref value"
+pref("parse.error", +)",
+    "test:2: prefs parse error: expected integer literal after '+'\n"
   );
 
   P(R"(
-pref("parse.error", true)
-pref("parse.error", true)
-    )",
-    "test:3: prefs parse error: expected ';' after ')'"
+pref("parse.error", true)",
+    "test:2: prefs parse error: expected ')' after pref value\n"
+  );
+
+  P(R"(
+pref("parse.error", true))",
+    "test:2: prefs parse error: expected ';' after ')'\n"
   );
 
   // This is something we saw in practice with the old parser, which allowed
   // repeated semicolons.
   P(R"(
 pref("parse.error", true);;
+pref("parse.error", true);;;
+pref("parse.error", true);;;;
+pref("int.ok", 0);
     )",
-    "test:2: prefs parse error: expected pref specifier at start of pref definition"
+    "test:2: prefs parse error: expected pref specifier at start of pref definition\n"
+    "test:3: prefs parse error: expected pref specifier at start of pref definition\n"
+    "test:3: prefs parse error: expected pref specifier at start of pref definition\n"
+    "test:4: prefs parse error: expected pref specifier at start of pref definition\n"
+    "test:4: prefs parse error: expected pref specifier at start of pref definition\n"
+    "test:4: prefs parse error: expected pref specifier at start of pref definition\n"
   );
 
   //-------------------------------------------------------------------------
   // Invalid syntax after various newline combinations, for the purpose of
   // testing that line numbers are correct.
   //-------------------------------------------------------------------------
 
   // In all of the following we have a \n, a \r, a \r\n, and then an error, so
   // the error is on line 4. (Note: these ones don't use raw string literals
   // because MSVC somehow swallows any \r that appears in them.)
 
   P("\n \r \r\n bad",
-    "test:4: prefs parse error: unknown keyword"
+    "test:4: prefs parse error: unknown keyword\n"
   );
 
   P("#\n#\r#\r\n bad",
-    "test:4: prefs parse error: unknown keyword"
+    "test:4: prefs parse error: unknown keyword\n"
   );
 
   P("//\n//\r//\r\n bad",
-    "test:4: prefs parse error: unknown keyword"
+    "test:4: prefs parse error: unknown keyword\n"
   );
 
   P("/*\n \r \r\n*/ bad",
-    "test:4: prefs parse error: unknown keyword"
+    "test:4: prefs parse error: unknown keyword\n"
   );
 
   // Note: the escape sequences do *not* affect the line number.
   P("pref(\"foo\\n\n foo\\r\r foo\\r\\n\r\n foo\", bad);",
-    "test:4: prefs parse error: unknown keyword"
+    "test:4: prefs parse error: unknown keyword\n"
   );
 
   // clang-format on
 }
--- a/netwerk/base/nsInputStreamPump.cpp
+++ b/netwerk/base/nsInputStreamPump.cpp
@@ -665,17 +665,17 @@ nsInputStreamPump::CallOnStateStop()
 uint32_t
 nsInputStreamPump::OnStateStop()
 {
     mMutex.AssertCurrentThreadIn();
 
     if (!NS_IsMainThread()) {
         // This method can be called on a different thread if nsInputStreamPump
         // is used off the main-thread.
-        nsresult rv = NS_DispatchToMainThread(
+        nsresult rv = mLabeledMainThreadTarget->Dispatch(
           NewRunnableMethod("nsInputStreamPump::CallOnStateStop",
                             this,
                             &nsInputStreamPump::CallOnStateStop));
         NS_ENSURE_SUCCESS(rv, STATE_IDLE);
         return STATE_IDLE;
     }
 
     AUTO_PROFILER_LABEL("nsInputStreamPump::OnStateStop", NETWORK);
--- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp
@@ -4589,17 +4589,18 @@ nsHalfOpenSocket::SetFastOpenConnected(n
     // In all other cases mConnectionNegotiatingFastOpen must not be nullptr.
     if (!mConnectionNegotiatingFastOpen) {
         return;
     }
 
     MOZ_ASSERT((mFastOpenStatus == TFO_NOT_TRIED) || 
                (mFastOpenStatus == TFO_DATA_SENT) ||
                (mFastOpenStatus == TFO_TRIED) ||
-               (mFastOpenStatus == TFO_DATA_COOKIE_NOT_ACCEPTED));
+               (mFastOpenStatus == TFO_DATA_COOKIE_NOT_ACCEPTED) ||
+               (mFastOpenStatus == TFO_DISABLED));
 
     RefPtr<nsHalfOpenSocket> deleteProtector(this);
 
     mEnt->mDoNotDestroy = true;
 
     // Delete 2 points of entry to FastOpen function so that we do not reenter.
     mEnt->mHalfOpenFastOpenBackups.RemoveElement(this);
     mSocketTransport->SetFastOpenCallback(nullptr);
--- a/taskcluster/ci/test/reftest.yml
+++ b/taskcluster/ci/test/reftest.yml
@@ -93,18 +93,18 @@ reftest:
             default: null
     instance-size:
         by-test-platform:
             android.*: xlarge
             default: default
     virtualization: virtual-with-gpu
     chunks:
         by-test-platform:
-            android-4.3-arm7-api-16/debug: 48
-            android.*: 24
+            android-4.3-arm7-api-16/debug: 56
+            android.*: 28
             macosx64.*/opt: 1
             macosx64.*/debug: 2
             windows10-64.*/opt: 1
             windows10-64.*/debug: 2
             default: 8
     e10s:
         by-test-platform:
             linux32/debug: both
--- a/taskcluster/docker/recipes/install-mercurial.sh
+++ b/taskcluster/docker/recipes/install-mercurial.sh
@@ -8,17 +8,17 @@
 set -e
 
 # Detect OS.
 if [ -f /etc/lsb-release ]; then
     # Disabled so linting works on Mac
     # shellcheck disable=SC1091
     . /etc/lsb-release
 
-    if [ "${DISTRIB_ID}" = "Ubuntu" ] && [ "${DISTRIB_RELEASE}" = "16.04" ]
+    if [ "${DISTRIB_ID}" = "Ubuntu" ] && [[ "${DISTRIB_RELEASE}" = "16.04" || "${DISTRIB_RELEASE}" = "17.10" ]]
     then
         HG_DEB=1
         HG_DIGEST=458746bd82b4732c72c611f1041f77a47a683bc75ff3f6ab7ed86ea394f48d94cd7e2d3d1d5b020906318a9a24bea27401a3a63d7e645514dbc2cb581621977f
         HG_SIZE=193710
         HG_FILENAME=mercurial_4.4.2_amd64.deb
 
         HG_COMMON_DIGEST=8074efbfff974f0bbdd0c3be3d272cc7a634456921e04db31369fbec1c9256ddaf44bdbe120f6f33113d2be9324a1537048028ebaaf205c6659e476a757358fd
         HG_COMMON_SIZE=2097892
--- a/taskcluster/docker/update-verify/Dockerfile
+++ b/taskcluster/docker/update-verify/Dockerfile
@@ -1,9 +1,12 @@
-FROM ubuntu:16.04
+# Ideally we'd use LTS here, but 16.04 doesn't have a new enough
+# p7zip-full for us. We should change this to the next LTS
+# (18.04), once available
+FROM ubuntu:17.10
 
 MAINTAINER release@mozilla.com
 
 RUN dpkg --add-architecture i386 && apt-get -q update \
     # p7zip-full is for extracting Windows and OS X packages
     # wget is for downloading update.xml, installers, and MARs
     # libgtk-3-0 and libgtk2.0-0 are required to run the Firefox updater
     && apt-get -q --yes install p7zip-full wget libgtk-3-0 libgtk-3.0:i386 libgtk2.0-0 libgtk2.0-0:i386 \
--- a/toolkit/components/extensions/ext-theme.js
+++ b/toolkit/components/extensions/ext-theme.js
@@ -154,19 +154,16 @@ class Theme {
         case "toolbar_field_text":
         case "toolbar_field_border":
         case "toolbar_field_separator":
         case "toolbar_top_separator":
         case "toolbar_bottom_separator":
         case "toolbar_vertical_separator":
         case "button_background_hover":
         case "button_background_active":
-        case "popup":
-        case "popup_text":
-        case "popup_border":
           this.lwtStyles[color] = cssColor;
           break;
       }
     }
   }
 
   /**
    * Helper method for loading images found in the extension's manifest.
--- a/toolkit/components/extensions/schemas/theme.json
+++ b/toolkit/components/extensions/schemas/theme.json
@@ -147,28 +147,16 @@
               },
               "button_background_hover": {
                "$ref": "ThemeColor",
                "optional": true
               },
               "button_background_active": {
                 "$ref": "ThemeColor",
                 "optional": true
-              },
-              "popup": {
-                "$ref": "ThemeColor",
-                "optional": true
-              },
-              "popup_text": {
-                "$ref": "ThemeColor",
-                "optional": true
-              },
-              "popup_border": {
-                "$ref": "ThemeColor",
-                "optional": true
               }
             },
             "additionalProperties": { "$ref": "UnrecognizedProperty" }
           },
           "icons": {
             "type": "object",
             "optional": true,
             "properties": {
--- a/toolkit/components/extensions/test/browser/browser.ini
+++ b/toolkit/components/extensions/test/browser/browser.ini
@@ -15,9 +15,8 @@ support-files =
 [browser_ext_themes_separators.js]
 [browser_ext_themes_static_onUpdated.js]
 [browser_ext_themes_tab_loading.js]
 [browser_ext_themes_tab_text.js]
 [browser_ext_themes_toolbar_fields.js]
 [browser_ext_themes_toolbars.js]
 [browser_ext_themes_toolbarbutton_icons.js]
 [browser_ext_themes_toolbarbutton_colors.js]
-[browser_ext_themes_arrowpanels.js]
deleted file mode 100644
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_arrowpanels.js
+++ /dev/null
@@ -1,79 +0,0 @@
-"use strict";
-
-function openIdentityPopup() {
-  let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
-  gIdentityHandler._identityBox.click();
-  return promise;
-}
-
-function closeIdentityPopup() {
-  let promise = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
-  gIdentityHandler._identityPopup.hidePopup();
-  return promise;
-}
-
-// This test checks applied WebExtension themes that attempt to change
-// popup properties
-
-add_task(async function test_popup_styling(browser, accDoc) {
-  const POPUP_BACKGROUND_COLOR = "#FF0000";
-  const POPUP_TEXT_COLOR = "#008000";
-  const POPUP_BORDER_COLOR = "#0000FF";
-
-  let extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      "theme": {
-        "images": {
-          "headerURL": "image1.png",
-        },
-        "colors": {
-          "accentcolor": ACCENT_COLOR,
-          "textcolor": TEXT_COLOR,
-          "popup": POPUP_BACKGROUND_COLOR,
-          "popup_text": POPUP_TEXT_COLOR,
-          "popup_border": POPUP_BORDER_COLOR,
-        },
-      },
-    },
-    files: {
-      "image1.png": BACKGROUND,
-    },
-  });
-
-
-  await BrowserTestUtils.withNewTab({gBrowser, url: "https://example.com"}, async function(browser) {
-    await extension.startup();
-
-    // Open the information arrow panel
-    await openIdentityPopup();
-
-    let arrowContent = document.getAnonymousElementByAttribute(gIdentityHandler._identityPopup, "class", "panel-arrowcontent");
-    let arrowContentComputedStyle = window.getComputedStyle(arrowContent);
-    // Ensure popup background color was set properly
-    Assert.equal(
-      arrowContentComputedStyle.getPropertyValue("background-color"),
-      `rgb(${hexToRGB(POPUP_BACKGROUND_COLOR).join(", ")})`,
-      "Popup background color should have been themed"
-    );
-
-    // Ensure popup text color was set properly
-    Assert.equal(
-      arrowContentComputedStyle.getPropertyValue("color"),
-      `rgb(${hexToRGB(POPUP_TEXT_COLOR).join(", ")})`,
-      "Popup text color should have been themed"
-    );
-
-    // Ensure popup border color was set properly
-    if (AppConstants.platform == "macosx") {
-      Assert.ok(
-        arrowContentComputedStyle.getPropertyValue("box-shadow").includes(`rgb(${hexToRGB(POPUP_BORDER_COLOR).join(", ")})`),
-        "Popup border color should be set"
-      );
-    } else {
-      testBorderColor(arrowContent, POPUP_BORDER_COLOR);
-    }
-
-    await closeIdentityPopup();
-    await extension.unload();
-  });
-});
--- a/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
+++ b/toolkit/components/extensions/test/browser/browser_ext_themes_toolbar_fields.js
@@ -5,16 +5,31 @@
 
 add_task(async function setup() {
   await SpecialPowers.pushPrefEnv({set: [
     ["extensions.webextensions.themes.enabled", true],
     ["browser.search.widget.inNavBar", true],
   ]});
 });
 
+function testBorderColor(element, expected) {
+  Assert.equal(window.getComputedStyle(element).borderLeftColor,
+               hexToCSS(expected),
+               "Field left border color should be set.");
+  Assert.equal(window.getComputedStyle(element).borderRightColor,
+               hexToCSS(expected),
+               "Field right border color should be set.");
+  Assert.equal(window.getComputedStyle(element).borderTopColor,
+               hexToCSS(expected),
+               "Field top border color should be set.");
+  Assert.equal(window.getComputedStyle(element).borderBottomColor,
+               hexToCSS(expected),
+               "Field bottom border color should be set.");
+}
+
 add_task(async function test_support_toolbar_field_properties() {
   const TOOLBAR_FIELD_BACKGROUND = "#ff00ff";
   const TOOLBAR_FIELD_COLOR = "#00ff00";
   const TOOLBAR_FIELD_BORDER = "#aaaaff";
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "theme": {
         "images": {
--- a/toolkit/components/extensions/test/browser/head.js
+++ b/toolkit/components/extensions/test/browser/head.js
@@ -1,10 +1,10 @@
 /* exported ACCENT_COLOR, BACKGROUND, ENCODED_IMAGE_DATA, FRAME_COLOR, TAB_TEXT_COLOR,
-   TEXT_COLOR, BACKGROUND_TAB_TEXT_COLOR, imageBufferFromDataURI, hexToCSS, hexToRGB, testBorderColor */
+   TEXT_COLOR, BACKGROUND_TAB_TEXT_COLOR, imageBufferFromDataURI, hexToCSS, hexToRGB */
 
 "use strict";
 
 const BACKGROUND = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0" +
   "DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
 const ENCODED_IMAGE_DATA = "iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0h" +
   "STQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAdhwAAHYcBj+XxZQAAB5dJREFUSMd" +
   "91vmTlEcZB/Bvd7/vO+/ce83O3gfLDUsC4VgIghBUEo2GM9GCFTaQBEISA1qIEVNQ4aggJDGIgAGTlFUKKcqKQpVHaQyny7FrCMiywp4ze+/Mzs67M/P" +
@@ -48,24 +48,8 @@ function rgbToCSS(rgb) {
 function hexToCSS(hex) {
   return rgbToCSS(hexToRGB(hex));
 }
 
 function imageBufferFromDataURI(encodedImageData) {
   let decodedImageData = atob(encodedImageData);
   return Uint8Array.from(decodedImageData, byte => byte.charCodeAt(0)).buffer;
 }
-
-function testBorderColor(element, expected) {
-  let computedStyle = window.getComputedStyle(element);
-  Assert.equal(computedStyle.borderLeftColor,
-               hexToCSS(expected),
-               "Element left border color should be set.");
-  Assert.equal(computedStyle.borderRightColor,
-               hexToCSS(expected),
-               "Element right border color should be set.");
-  Assert.equal(computedStyle.borderTopColor,
-               hexToCSS(expected),
-               "Element top border color should be set.");
-  Assert.equal(computedStyle.borderBottomColor,
-               hexToCSS(expected),
-               "Element bottom border color should be set.");
-}
--- a/xpcom/io/nsBinaryStream.cpp
+++ b/xpcom/io/nsBinaryStream.cpp
@@ -192,25 +192,25 @@ nsBinaryOutputStream::Write64(uint64_t a
     return NS_ERROR_FAILURE;
   }
   return rv;
 }
 
 NS_IMETHODIMP
 nsBinaryOutputStream::WriteFloat(float aFloat)
 {
-  NS_ASSERTION(sizeof(float) == sizeof(uint32_t),
-               "False assumption about sizeof(float)");
+  static_assert(sizeof(float) == sizeof(uint32_t),
+                "False assumption about sizeof(float)");
   return Write32(*reinterpret_cast<uint32_t*>(&aFloat));
 }
 
 NS_IMETHODIMP
 nsBinaryOutputStream::WriteDouble(double aDouble)
 {
-  NS_ASSERTION(sizeof(double) == sizeof(uint64_t),
+  static_assert(sizeof(double) == sizeof(uint64_t),
                "False assumption about sizeof(double)");
   return Write64(*reinterpret_cast<uint64_t*>(&aDouble));
 }
 
 NS_IMETHODIMP
 nsBinaryOutputStream::WriteStringZ(const char* aString)
 {
   uint32_t length;
@@ -365,21 +365,19 @@ nsBinaryOutputStream::WriteID(const nsII
     return rv;
   }
 
   rv = Write16(aIID.m2);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  for (int i = 0; i < 8; ++i) {
-    rv = Write8(aIID.m3[i]);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
+  rv = WriteBytes(reinterpret_cast<const char*>(&aIID.m3[0]), sizeof(aIID.m3));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
-    }
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP_(char*)
 nsBinaryOutputStream::GetBuffer(uint32_t aLength, uint32_t aAlignMask)
 {
@@ -620,25 +618,25 @@ nsBinaryInputStream::Read64(uint64_t* aN
   }
   *aNum = mozilla::NativeEndian::swapFromBigEndian(*aNum);
   return rv;
 }
 
 NS_IMETHODIMP
 nsBinaryInputStream::ReadFloat(float* aFloat)
 {
-  NS_ASSERTION(sizeof(float) == sizeof(uint32_t),
-               "False assumption about sizeof(float)");
+  static_assert(sizeof(float) == sizeof(uint32_t),
+                "False assumption about sizeof(float)");
   return Read32(reinterpret_cast<uint32_t*>(aFloat));
 }
 
 NS_IMETHODIMP
 nsBinaryInputStream::ReadDouble(double* aDouble)
 {
-  NS_ASSERTION(sizeof(double) == sizeof(uint64_t),
+  static_assert(sizeof(double) == sizeof(uint64_t),
                "False assumption about sizeof(double)");
   return Read64(reinterpret_cast<uint64_t*>(aDouble));
 }
 
 static nsresult
 WriteSegmentToCString(nsIInputStream* aStream,
                       void* aClosure,
                       const char* aFromSegment,
@@ -709,17 +707,17 @@ static nsresult
 WriteSegmentToString(nsIInputStream* aStream,
                      void* aClosure,
                      const char* aFromSegment,
                      uint32_t aToOffset,
                      uint32_t aCount,
                      uint32_t* aWriteCount)
 {
   NS_PRECONDITION(aCount > 0, "Why are we being told to write 0 bytes?");
-  NS_PRECONDITION(sizeof(char16_t) == 2, "We can't handle other sizes!");
+  static_assert(sizeof(char16_t) == 2, "We can't handle other sizes!");
 
   WriteStringClosure* closure = static_cast<WriteStringClosure*>(aClosure);
   char16_t* cursor = closure->mWriteCursor;
 
   // we're always going to consume the whole buffer no matter what
   // happens, so take care of that right now.. that allows us to
   // tweak aCount later. Do NOT move this!
   *aWriteCount = aCount;
@@ -1004,21 +1002,24 @@ nsBinaryInputStream::ReadID(nsID* aResul
     return rv;
   }
 
   rv = Read16(&aResult->m2);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
-  for (int i = 0; i < 8; ++i) {
-    rv = Read8(&aResult->m3[i]);
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
-    }
+  const uint32_t toRead = sizeof(aResult->m3);
+  uint32_t bytesRead = 0;
+  rv = Read(reinterpret_cast<char*>(&aResult->m3[0]), toRead, &bytesRead);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  if (bytesRead != toRead) {
+    return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP_(char*)
 nsBinaryInputStream::GetBuffer(uint32_t aLength, uint32_t aAlignMask)
 {