WIP: Bug 1723204: Infrastructure for causing a crash at most N times r=KrisWright,ckerschb,freddyb
☠☠ backed out by 8cd007fdbd4b ☠ ☠
authorTom Ritter <tom@mozilla.com>
Tue, 24 Aug 2021 11:20:44 +0000
changeset 589740 d7e4042af76c2c662aede1c7e3356b727eaa6171
parent 589739 b3a5889e014829dff890b9c2e7c25c822321894a
child 589741 4a20d1832cf46da3540ea232828e637316ff4549
push id148517
push usertritter@mozilla.com
push dateTue, 24 Aug 2021 11:23:25 +0000
treeherderautoland@0dab9553a2a8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersKrisWright, ckerschb, freddyb
bugs1723204
milestone93.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
WIP: Bug 1723204: Infrastructure for causing a crash at most N times r=KrisWright,ckerschb,freddyb Differential Revision: https://phabricator.services.mozilla.com/D121416
dom/security/nsContentSecurityUtils.cpp
dom/security/nsContentSecurityUtils.h
dom/security/test/gtest/TestSmartCrashTrimmer.cpp
dom/security/test/gtest/moz.build
modules/libpref/Preferences.h
--- a/dom/security/nsContentSecurityUtils.cpp
+++ b/dom/security/nsContentSecurityUtils.cpp
@@ -198,16 +198,76 @@ nsresult RegexEval(const nsAString& aPat
     aRegexResults->AppendElement(value);
   }
 
   aMatchResult = true;
   return NS_OK;
 }
 
 /*
+ * MOZ_CRASH_UNSAFE_PRINTF has a sPrintfCrashReasonSize-sized buffer. We need
+ * to make sure we don't exceed it.  These functions perform this check and
+ * munge things for us.
+ *
+ */
+
+/*
+ * Destructively truncates a string to fit within the limit
+ */
+char* nsContentSecurityUtils::SmartFormatCrashString(const char* str) {
+  return nsContentSecurityUtils::SmartFormatCrashString(strdup(str));
+}
+
+char* nsContentSecurityUtils::SmartFormatCrashString(char* str) {
+  auto str_len = strlen(str);
+
+  if (str_len > sPrintfCrashReasonSize) {
+    str[sPrintfCrashReasonSize - 1] = '\0';
+    str_len = strlen(str);
+  }
+  MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize > str_len);
+
+  return str;
+}
+
+/*
+ * Destructively truncates two strings to fit within the limit.
+ * format_string is a format string containing two %s entries
+ * The second string will be truncated to the _last_ 25 characters
+ * The first string will be truncated to the remaining limit.
+ */
+nsCString nsContentSecurityUtils::SmartFormatCrashString(
+    const char* part1, const char* part2, const char* format_string) {
+  return SmartFormatCrashString(strdup(part1), strdup(part2), format_string);
+}
+
+nsCString nsContentSecurityUtils::SmartFormatCrashString(
+    char* part1, char* part2, const char* format_string) {
+  auto part1_len = strlen(part1);
+  auto part2_len = strlen(part2);
+
+  auto constant_len = strlen(format_string) - 4;
+
+  if (part1_len + part2_len + constant_len > sPrintfCrashReasonSize) {
+    if (part2_len > 25) {
+      part2 += (part2_len - 25);
+    }
+    part2_len = strlen(part2);
+
+    part1[sPrintfCrashReasonSize - (constant_len + part2_len + 1)] = '\0';
+    part1_len = strlen(part1);
+  }
+  MOZ_RELEASE_ASSERT(sPrintfCrashReasonSize >
+                     constant_len + part1_len + part2_len);
+
+  auto parts = nsPrintfCString(format_string, part1, part2);
+  return std::move(parts);
+}
+
+/*
  * Telemetry Events extra data only supports 80 characters, so we optimize the
  * filename to be smaller and collect more data.
  */
 nsString OptimizeFileName(const nsAString& aFileName) {
   nsString optimizedName(aFileName);
 
   MOZ_LOG(
       sCSMLog, LogLevel::Verbose,
@@ -400,16 +460,66 @@ FilenameTypeAndDetails nsContentSecurity
                                     Some(strSanitizedPath));
     }
   }
 #endif
 
   return FilenameTypeAndDetails(kOther, Nothing());
 }
 
+#ifdef NIGHTLY_BUILD
+// Crash String must be safe from a telemetry point of view.
+// This will be ensured when this function is used.
+void PossiblyCrash(const char* pref_suffix, const nsCString crash_string) {
+  if (MOZ_UNLIKELY(!XRE_IsParentProcess())) {
+    // We only crash in the parent (unfortunately) because it's
+    // the only place we can be sure that our only-crash-once
+    // pref-writing works.
+    return;
+  }
+
+  nsCString previous_crashes("security.crash_tracking.");
+  previous_crashes.Append(pref_suffix);
+  previous_crashes.Append(".prevCrashes");
+
+  nsCString max_crashes("security.crash_tracking.");
+  max_crashes.Append(pref_suffix);
+  max_crashes.Append(".maxCrashes");
+
+  int32_t numberOfPreviousCrashes = 0;
+  numberOfPreviousCrashes = Preferences::GetInt(previous_crashes.get(), 0);
+
+  int32_t maxAllowableCrashes = 0;
+  maxAllowableCrashes = Preferences::GetInt(max_crashes.get(), 0);
+
+  if (numberOfPreviousCrashes >= maxAllowableCrashes) {
+    return;
+  }
+
+  nsresult rv =
+      Preferences::SetInt(previous_crashes.get(), ++numberOfPreviousCrashes);
+  if (NS_FAILED(rv)) {
+    return;
+  }
+
+  nsCOMPtr<nsIPrefService> prefsCom = Preferences::GetService();
+  Preferences* prefs = static_cast<Preferences*>(prefsCom.get());
+
+  if (!prefs->AllowOffMainThreadSave()) {
+    // Do not crash if we can't save prefs off the main thread
+    return;
+  }
+
+  rv = prefs->SavePrefFileBlocking();
+  if (!NS_FAILED(rv)) {
+    MOZ_CRASH_UNSAFE_PRINTF("%s", nsContentSecurityUtils::SmartFormatCrashString(crash_string.get()));
+  }
+}
+#endif
+
 class EvalUsageNotificationRunnable final : public Runnable {
  public:
   EvalUsageNotificationRunnable(bool aIsSystemPrincipal,
                                 NS_ConvertUTF8toUTF16& aFileNameA,
                                 uint64_t aWindowID, uint32_t aLineNumber,
                                 uint32_t aColumnNumber)
       : mozilla::Runnable("EvalUsageNotificationRunnable"),
         mIsSystemPrincipal(aIsSystemPrincipal),
@@ -565,32 +675,23 @@ bool nsContentSecurityUtils::IsEvalAllow
   // Log to MOZ_LOG
   MOZ_LOG(sCSMLog, LogLevel::Warning,
           ("Blocking eval() %s from file %s and script "
            "provided %s",
            (aIsSystemPrincipal ? "with System Principal" : "in parent process"),
            fileName.get(), NS_ConvertUTF16toUTF8(aScript).get()));
 
   // Maybe Crash
-#ifdef DEBUG
-  // MOZ_CRASH_UNSAFE_PRINTF gives us at most 1024 characters to print.
-  // The given string literal leaves us with ~950, so I'm leaving
-  // each 475 for fileName and aScript each.
-  if (fileName.Length() > 475) {
-    fileName.SetLength(475);
-  }
-  nsAutoCString trimmedScript = NS_ConvertUTF16toUTF8(aScript);
-  if (trimmedScript.Length() > 475) {
-    trimmedScript.SetLength(475);
-  }
-  MOZ_CRASH_UNSAFE_PRINTF(
-      "Blocking eval() %s from file %s and script provided "
-      "%s",
-      (aIsSystemPrincipal ? "with System Principal" : "in parent process"),
-      fileName.get(), trimmedScript.get());
+#if defined(DEBUG)
+  auto crashString = nsContentSecurityUtils::SmartFormatCrashString(
+      NS_ConvertUTF16toUTF8(aScript).get(), fileName.get(),
+      (aIsSystemPrincipal
+           ? "Blocking eval() with System Principal with script %s from file %s"
+           : "Blocking eval() in parent process with script %s from file %s"));
+  MOZ_CRASH_UNSAFE_PRINTF("%s", crashString.get());
 #endif
 
   return false;
 }
 
 /* static */
 void nsContentSecurityUtils::NotifyEvalUsage(bool aIsSystemPrincipal,
                                              NS_ConvertUTF8toUTF16& aFileNameA,
--- a/dom/security/nsContentSecurityUtils.h
+++ b/dom/security/nsContentSecurityUtils.h
@@ -32,18 +32,16 @@ class nsContentSecurityUtils {
   // CSPs upgrade-insecure-requests directive applies to same origin top level
   // navigations. Using the SOP would return false for the case when an https
   // page triggers and http page to load, even though that http page would be
   // upgraded to https later. Hence we have to use that custom function instead
   // of simply calling aTriggeringPrincipal->Equals(aResultPrincipal).
   static bool IsConsideredSameOriginForUIR(nsIPrincipal* aTriggeringPrincipal,
                                            nsIPrincipal* aResultPrincipal);
 
-  static FilenameTypeAndDetails FilenameToFilenameType(
-      const nsString& fileName, bool collectAdditionalExtensionData);
   static bool IsEvalAllowed(JSContext* cx, bool aIsSystemPrincipal,
                             const nsAString& aScript);
   static void NotifyEvalUsage(bool aIsSystemPrincipal,
                               NS_ConvertUTF8toUTF16& aFileNameA,
                               uint64_t aWindowID, uint32_t aLineNumber,
                               uint32_t aColumnNumber);
 
   // Helper function for various checks:
@@ -62,16 +60,26 @@ class nsContentSecurityUtils {
   // * x-frame-options
   // If any of the two disallows framing, the channel will be cancelled.
   static void PerformCSPFrameAncestorAndXFOCheck(nsIChannel* aChannel);
 
   // Helper function to Check if a Download is allowed;
   static long ClassifyDownload(nsIChannel* aChannel,
                                const nsAutoCString& aMimeTypeGuess);
 
+  // Public only for testing
+  static FilenameTypeAndDetails FilenameToFilenameType(
+      const nsString& fileName, bool collectAdditionalExtensionData);
+  static char* SmartFormatCrashString(const char* str);
+  static char* SmartFormatCrashString(char* str);
+  static nsCString SmartFormatCrashString(const char* part1, const char* part2,
+                                          const char* format_string);
+  static nsCString SmartFormatCrashString(char* part1, char* part2,
+                                          const char* format_string);
+
 #if defined(DEBUG)
   static void AssertAboutPageHasCSP(mozilla::dom::Document* aDocument);
 #endif
 
   static bool ValidateScriptFilename(const char* aFilename,
                                      bool aIsSystemRealm);
   // Helper Function to Post a message to the corresponding JS-Console
   static void LogMessageToConsole(nsIHttpChannel* aChannel, const char* aMsg);
new file mode 100644
--- /dev/null
+++ b/dom/security/test/gtest/TestSmartCrashTrimmer.cpp
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "gtest/gtest.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "nsContentSecurityUtils.h"
+#include "nsStringFwd.h"
+
+#define ASSERT_STRCMP(first, second) ASSERT_TRUE(strcmp(first, second) == 0);
+
+#define ASSERT_STRCMP_AND_PRINT(first, second)             \
+  fprintf(stderr, "First: %s\n", first);                   \
+  fprintf(stderr, "Second: %s\n", second);                 \
+  fprintf(stderr, "strcmp = %i\n", strcmp(first, second)); \
+  ASSERT_EQUAL(first, second);
+
+TEST(SmartCrashTrimmer, Test)
+{
+  static_assert(sPrintfCrashReasonSize == 1024);
+  {
+    auto ret = nsContentSecurityUtils::SmartFormatCrashString(
+        std::string(1025, '.').c_str());
+    ASSERT_EQ(strlen(ret), 1023ul);
+  }
+
+  {
+    auto ret = nsContentSecurityUtils::SmartFormatCrashString(
+        std::string(1025, '.').c_str(), std::string(1025, 'A').c_str(),
+        "Hello %s world %s!");
+    char expected[1025];
+    sprintf(expected, "Hello %s world AAAAAAAAAAAAAAAAAAAAAAAAA!",
+            std::string(984, '.').c_str());
+    ASSERT_STRCMP(ret.get(), expected);
+  }
+}
--- a/dom/security/test/gtest/moz.build
+++ b/dom/security/test/gtest/moz.build
@@ -3,16 +3,17 @@
 # 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/.
 
 UNIFIED_SOURCES += [
     "TestCSPParser.cpp",
     "TestFilenameEvalParser.cpp",
     "TestSecureContext.cpp",
+    "TestSmartCrashTrimmer.cpp",
 ]
 
 if CONFIG["OS_TARGET"] != "Android":
     UNIFIED_SOURCES += [
         "TestUnexpectedPrivilegedLoads.cpp",
     ]
 
 FINAL_LIBRARY = "xul-gtest"
--- a/modules/libpref/Preferences.h
+++ b/modules/libpref/Preferences.h
@@ -416,16 +416,19 @@ class Preferences final : public nsIPref
 
   // Explicitly choosing synchronous or asynchronous (if allowed) preferences
   // file write. Only for the default file.  The guarantee for the "blocking"
   // is that when it returns, the file on disk reflect the current state of
   // preferences.
   nsresult SavePrefFileBlocking();
   nsresult SavePrefFileAsynchronous();
 
+  // If this is false, only blocking writes, on main thread are allowed.
+  bool AllowOffMainThreadSave();
+
  private:
   virtual ~Preferences();
 
   nsresult NotifyServiceObservers(const char* aSubject);
 
   // Loads the prefs.js file from the profile, or creates a new one. Returns
   // the prefs file if successful, or nullptr on failure.
   already_AddRefed<nsIFile> ReadSavedPrefs();
@@ -437,19 +440,16 @@ class Preferences final : public nsIPref
 
   // Default pref file save can be blocking or not.
   enum class SaveMethod { Blocking, Asynchronous };
 
   // Off main thread is only respected for the default aFile value (nullptr).
   nsresult SavePrefFileInternal(nsIFile* aFile, SaveMethod aSaveMethod);
   nsresult WritePrefFile(nsIFile* aFile, SaveMethod aSaveMethod);
 
-  // If this is false, only blocking writes, on main thread are allowed.
-  bool AllowOffMainThreadSave();
-
   // Helpers for implementing
   // Register(Prefix)Callback/Unregister(Prefix)Callback.
  public:
   // Public so the ValueObserver classes can use it.
   enum MatchKind {
     PrefixMatch,
     ExactMatch,
   };