Bug 418986 - Resist fingerprinting by preventing exposure of screen and system info. r=mrbkap, r=heycam
authorKathy Brade <brade@pearlcrescent.com>
Sun, 07 Jun 2015 09:02:00 -0400
changeset 247629 3abb08512b2435c53e63dcba2843d0df66391a55
parent 247628 a9cff1d9e7c6a9a698522ee4cd9ddbac39cb0b07
child 247630 98ada2157a8e70798186e3a9bf15b3a9832fab0c
push id60759
push userryanvm@gmail.com
push dateMon, 08 Jun 2015 17:14:55 +0000
treeherdermozilla-inbound@98ada2157a8e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmrbkap, heycam
bugs418986
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 418986 - Resist fingerprinting by preventing exposure of screen and system info. r=mrbkap, r=heycam
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/base/nsGlobalWindow.cpp
dom/base/nsGlobalWindow.h
dom/base/nsScreen.cpp
dom/base/nsScreen.h
dom/base/test/chrome/bug418986-1.js
dom/base/test/chrome/chrome.ini
dom/base/test/chrome/test_bug418986-1.xul
dom/base/test/mochitest.ini
dom/base/test/test_bug418986-1.html
dom/events/Event.cpp
dom/events/test/bug418986-3.js
dom/events/test/chrome.ini
dom/events/test/mochitest.ini
dom/events/test/test_bug418986-3.html
dom/events/test/test_bug418986-3.xul
layout/style/nsComputedDOMStyle.cpp
layout/style/nsMediaFeatures.cpp
layout/style/test/chrome/bug418986-2.js
layout/style/test/chrome/chrome.ini
layout/style/test/chrome/test_bug418986-2.xul
layout/style/test/mochitest.ini
layout/style/test/test_bug418986-2.html
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -248,16 +248,17 @@ bool nsContentUtils::sInitialized = fals
 bool nsContentUtils::sIsFullScreenApiEnabled = false;
 bool nsContentUtils::sTrustedFullScreenOnly = true;
 bool nsContentUtils::sIsCutCopyAllowed = true;
 bool nsContentUtils::sIsPerformanceTimingEnabled = false;
 bool nsContentUtils::sIsResourceTimingEnabled = false;
 bool nsContentUtils::sIsUserTimingLoggingEnabled = false;
 bool nsContentUtils::sIsExperimentalAutocompleteEnabled = false;
 bool nsContentUtils::sEncodeDecodeURLHash = false;
+bool nsContentUtils::sPrivacyResistFingerprinting = false;
 
 uint32_t nsContentUtils::sHandlingInputTimeout = 1000;
 
 nsHtml5StringParser* nsContentUtils::sHTMLFragmentParser = nullptr;
 nsIParser* nsContentUtils::sXMLFragmentParser = nullptr;
 nsIFragmentContentSink* nsContentUtils::sXMLFragmentSink = nullptr;
 bool nsContentUtils::sFragmentParsingActive = false;
 
@@ -528,16 +529,19 @@ nsContentUtils::Init()
                                "dom.performance.enable_user_timing_logging", false);
 
   Preferences::AddBoolVarCache(&sIsExperimentalAutocompleteEnabled,
                                "dom.forms.autocomplete.experimental", false);
 
   Preferences::AddBoolVarCache(&sEncodeDecodeURLHash,
                                "dom.url.encode_decode_hash", false);
 
+  Preferences::AddBoolVarCache(&sPrivacyResistFingerprinting,
+                               "privacy.resistFingerprinting", false);
+
   Preferences::AddUintVarCache(&sHandlingInputTimeout,
                                "dom.event.handling-user-input-time-limit",
                                1000);
 
 #if !(defined(DEBUG) || defined(MOZ_ENABLE_JS_DUMP))
   Preferences::AddBoolVarCache(&sDOMWindowDumpEnabled,
                                "browser.dom.window.dump.enabled");
 #endif
@@ -1984,16 +1988,26 @@ nsContentUtils::IsCallerChrome()
   if (SubjectPrincipal() == sSystemPrincipal) {
     return true;
   }
 
   // If the check failed, look for UniversalXPConnect on the cx compartment.
   return xpc::IsUniversalXPConnectEnabled(GetCurrentJSContext());
 }
 
+bool
+nsContentUtils::ShouldResistFingerprinting(nsIDocShell* aDocShell)
+{
+  if (!aDocShell) {
+    return false;
+  }
+  bool isChrome = nsContentUtils::IsChromeDoc(aDocShell->GetDocument());
+  return !isChrome && sPrivacyResistFingerprinting;
+}
+
 namespace mozilla {
 namespace dom {
 namespace workers {
 extern bool IsCurrentThreadRunningChromeWorker();
 extern JSContext* GetCurrentThreadJSContext();
 }
 }
 }
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -194,16 +194,19 @@ public:
   static bool     IsCallerContentXBL();
 
   static bool     IsImageSrcSetDisabled();
 
   static bool LookupBindingMember(JSContext* aCx, nsIContent *aContent,
                                   JS::Handle<jsid> aId,
                                   JS::MutableHandle<JSPropertyDescriptor> aDesc);
 
+  // Check whether we should avoid leaking distinguishing information to JS/CSS.
+  static bool ShouldResistFingerprinting(nsIDocShell* aDocShell);
+
   /**
    * Returns the parent node of aChild crossing document boundaries.
    * Uses the parent node in the composed document.
    */
   static nsINode* GetCrossDocParentNode(nsINode* aChild);
 
   /**
    * Do not ever pass null pointers to this method.  If one of your
@@ -1908,16 +1911,26 @@ public:
    * Returns true if URL setters should percent encode the Hash/Ref segment
    * and getters should return the percent decoded value of the segment
    */
   static bool EncodeDecodeURLHash()
   {
     return sEncodeDecodeURLHash;
   }
 
+  /*
+   * Returns true if the browser should attempt to prevent content scripts
+   * from collecting distinctive information about the browser that could
+   * be used to "fingerprint" and track the user across websites.
+   */
+  static bool ResistFingerprinting()
+  {
+    return sPrivacyResistFingerprinting;
+  }
+
   /**
    * Returns true if the doc tree branch which contains aDoc contains any
    * plugins which we don't control event dispatch for, i.e. do any plugins
    * in the same tab as this document receive key events outside of our
    * control? This always returns false on MacOSX.
    */
   static bool HasPluginWithUncontrolledEventDispatch(nsIDocument* aDoc);
 
@@ -2448,16 +2461,17 @@ private:
   static bool sTrustedFullScreenOnly;
   static bool sIsCutCopyAllowed;
   static uint32_t sHandlingInputTimeout;
   static bool sIsPerformanceTimingEnabled;
   static bool sIsResourceTimingEnabled;
   static bool sIsUserTimingLoggingEnabled;
   static bool sIsExperimentalAutocompleteEnabled;
   static bool sEncodeDecodeURLHash;
+  static bool sPrivacyResistFingerprinting;
 
   static nsHtml5StringParser* sHTMLFragmentParser;
   static nsIParser* sXMLFragmentParser;
   static nsIFragmentContentSink* sXMLFragmentSink;
 
   /**
    * True if there's a fragment parser activation on the stack.
    */
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -4998,16 +4998,22 @@ nsGlobalWindow::SetInnerHeight(int32_t a
   return rv.StealNSResult();
 }
 
 nsIntSize
 nsGlobalWindow::GetOuterSize(ErrorResult& aError)
 {
   MOZ_ASSERT(IsOuterWindow());
 
+  if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) {
+    CSSIntSize size;
+    aError = GetInnerSize(size);
+    return nsIntSize(size.width, size.height);
+  }
+
   nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
   if (!treeOwnerAsWin) {
     aError.Throw(NS_ERROR_FAILURE);
     return nsIntSize(0, 0);
   }
 
   nsGlobalWindow* rootWindow =
     static_cast<nsGlobalWindow *>(GetPrivateRoot());
@@ -5162,16 +5168,21 @@ nsGlobalWindow::SetOuterHeight(int32_t a
   return rv.StealNSResult();
 }
 
 nsIntPoint
 nsGlobalWindow::GetScreenXY(ErrorResult& aError)
 {
   MOZ_ASSERT(IsOuterWindow());
 
+  // When resisting fingerprinting, always return (0,0)
+  if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) {
+    return nsIntPoint(0, 0);
+  }
+
   nsCOMPtr<nsIBaseWindow> treeOwnerAsWin = GetTreeOwnerWindow();
   if (!treeOwnerAsWin) {
     aError.Throw(NS_ERROR_FAILURE);
     return nsIntPoint(0, 0);
   }
 
   int32_t x = 0, y = 0;
   aError = treeOwnerAsWin->GetPosition(&x, &y);
@@ -5235,16 +5246,21 @@ nsGlobalWindow::GetInnerScreenRect()
   return rootFrame->GetScreenRectInAppUnits();
 }
 
 float
 nsGlobalWindow::GetMozInnerScreenX(ErrorResult& aError)
 {
   FORWARD_TO_OUTER_OR_THROW(GetMozInnerScreenX, (aError), aError, 0);
 
+  // When resisting fingerprinting, always return 0.
+  if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) {
+    return 0.0;
+  }
+
   nsRect r = GetInnerScreenRect();
   return nsPresContext::AppUnitsToFloatCSSPixels(r.x);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::GetMozInnerScreenX(float* aScreenX)
 {
   ErrorResult rv;
@@ -5253,16 +5269,21 @@ nsGlobalWindow::GetMozInnerScreenX(float
   return rv.StealNSResult();
 }
 
 float
 nsGlobalWindow::GetMozInnerScreenY(ErrorResult& aError)
 {
   FORWARD_TO_OUTER_OR_THROW(GetMozInnerScreenY, (aError), aError, 0);
 
+  // Return 0 to prevent fingerprinting.
+  if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) {
+    return 0.0;
+  }
+
   nsRect r = GetInnerScreenRect();
   return nsPresContext::AppUnitsToFloatCSSPixels(r.y);
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::GetMozInnerScreenY(float* aScreenY)
 {
   ErrorResult rv;
@@ -5281,16 +5302,20 @@ nsGlobalWindow::GetDevicePixelRatio(Erro
   }
 
   nsRefPtr<nsPresContext> presContext;
   mDocShell->GetPresContext(getter_AddRefs(presContext));
   if (!presContext) {
     return 1.0;
   }
 
+  if (nsContentUtils::ShouldResistFingerprinting(mDocShell)) {
+    return 1.0;
+  }
+
   return float(nsPresContext::AppUnitsPerCSSPixel())/
       presContext->AppUnitsPerDevPixel();
 }
 
 NS_IMETHODIMP
 nsGlobalWindow::GetDevicePixelRatio(float* aRatio)
 {
   ErrorResult rv;
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -1054,16 +1054,18 @@ public:
   void Back(mozilla::ErrorResult& aError);
   void Forward(mozilla::ErrorResult& aError);
   void Home(mozilla::ErrorResult& aError);
   bool Find(const nsAString& aString, bool aCaseSensitive, bool aBackwards,
             bool aWrapAround, bool aWholeWord, bool aSearchInFrames,
             bool aShowDialog, mozilla::ErrorResult& aError);
   uint64_t GetMozPaintCount(mozilla::ErrorResult& aError);
 
+  bool ShouldResistFingerprinting();
+
   mozilla::dom::MozSelfSupport* GetMozSelfSupport(mozilla::ErrorResult& aError);
 
   already_AddRefed<nsIDOMWindow> OpenDialog(JSContext* aCx,
                                             const nsAString& aUrl,
                                             const nsAString& aName,
                                             const nsAString& aOptions,
                                             const mozilla::dom::Sequence<JS::Value>& aExtraArgument,
                                             mozilla::ErrorResult& aError);
--- a/dom/base/nsScreen.cpp
+++ b/dom/base/nsScreen.cpp
@@ -63,16 +63,21 @@ NS_INTERFACE_MAP_BEGIN(nsScreen)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
 NS_IMPL_ADDREF_INHERITED(nsScreen, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(nsScreen, DOMEventTargetHelper)
 
 int32_t
 nsScreen::GetPixelDepth(ErrorResult& aRv)
 {
+  // Return 24 to prevent fingerprinting.
+  if (ShouldResistFingerprinting()) {
+    return 24;
+  }
+
   nsDeviceContext* context = GetDeviceContext();
 
   if (!context) {
     aRv.Throw(NS_ERROR_FAILURE);
     return -1;
   }
 
   uint32_t depth;
@@ -106,16 +111,21 @@ nsDeviceContext*
 nsScreen::GetDeviceContext()
 {
   return nsLayoutUtils::GetDeviceContextForScreenInfo(GetOwner());
 }
 
 nsresult
 nsScreen::GetRect(nsRect& aRect)
 {
+  // Return window inner rect to prevent fingerprinting.
+  if (ShouldResistFingerprinting()) {
+    return GetWindowInnerRect(aRect);
+  }
+
   nsDeviceContext *context = GetDeviceContext();
 
   if (!context) {
     return NS_ERROR_FAILURE;
   }
 
   context->GetRect(aRect);
 
@@ -125,16 +135,21 @@ nsScreen::GetRect(nsRect& aRect)
   aRect.width = nsPresContext::AppUnitsToIntCSSPixels(aRect.width);
 
   return NS_OK;
 }
 
 nsresult
 nsScreen::GetAvailRect(nsRect& aRect)
 {
+  // Return window inner rect to prevent fingerprinting.
+  if (ShouldResistFingerprinting()) {
+    return GetWindowInnerRect(aRect);
+  }
+
   nsDeviceContext *context = GetDeviceContext();
 
   if (!context) {
     return NS_ERROR_FAILURE;
   }
 
   context->GetClientRect(aRect);
 
@@ -161,32 +176,36 @@ nsScreen::Notify(const hal::ScreenConfig
   if (mOrientation != previousOrientation) {
     DispatchTrustedEvent(NS_LITERAL_STRING("mozorientationchange"));
   }
 }
 
 void
 nsScreen::GetMozOrientation(nsString& aOrientation)
 {
-  switch (mOrientation) {
-  case eScreenOrientation_PortraitPrimary:
-    aOrientation.AssignLiteral("portrait-primary");
-    break;
-  case eScreenOrientation_PortraitSecondary:
-    aOrientation.AssignLiteral("portrait-secondary");
-    break;
-  case eScreenOrientation_LandscapePrimary:
+  if (ShouldResistFingerprinting()) {
     aOrientation.AssignLiteral("landscape-primary");
-    break;
-  case eScreenOrientation_LandscapeSecondary:
-    aOrientation.AssignLiteral("landscape-secondary");
-    break;
-  case eScreenOrientation_None:
-  default:
-    MOZ_CRASH("Unacceptable mOrientation value");
+  } else {
+    switch (mOrientation) {
+    case eScreenOrientation_PortraitPrimary:
+      aOrientation.AssignLiteral("portrait-primary");
+      break;
+    case eScreenOrientation_PortraitSecondary:
+      aOrientation.AssignLiteral("portrait-secondary");
+      break;
+    case eScreenOrientation_LandscapePrimary:
+      aOrientation.AssignLiteral("landscape-primary");
+      break;
+    case eScreenOrientation_LandscapeSecondary:
+      aOrientation.AssignLiteral("landscape-secondary");
+      break;
+    case eScreenOrientation_None:
+    default:
+      MOZ_CRASH("Unacceptable mOrientation value");
+    }
   }
 }
 
 NS_IMETHODIMP
 nsScreen::GetSlowMozOrientation(nsAString& aOrientation)
 {
   nsString orientation;
   GetMozOrientation(orientation);
@@ -368,8 +387,32 @@ nsScreen::FullScreenEventListener::Handl
 
   target->RemoveSystemEventListener(NS_LITERAL_STRING("mozfullscreenchange"),
                                     this, true);
 
   hal::UnlockScreenOrientation();
 
   return NS_OK;
 }
+
+nsresult
+nsScreen::GetWindowInnerRect(nsRect& aRect)
+{
+  aRect.x = 0;
+  aRect.y = 0;
+  nsCOMPtr<nsIDOMWindow> win = GetOwner();
+  if (!win) {
+    return NS_ERROR_FAILURE;
+  }
+  nsresult rv = win->GetInnerWidth(&aRect.width);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return win->GetInnerHeight(&aRect.height);
+}
+
+bool nsScreen::ShouldResistFingerprinting() const
+{
+  bool resist = false;
+  nsCOMPtr<nsPIDOMWindow> owner = GetOwner();
+  if (owner) {
+    resist = nsContentUtils::ShouldResistFingerprinting(owner->GetDocShell());
+  }
+  return resist;
+}
--- a/dom/base/nsScreen.h
+++ b/dom/base/nsScreen.h
@@ -126,16 +126,17 @@ public:
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   void Notify(const mozilla::hal::ScreenConfiguration& aConfiguration) override;
 
 protected:
   nsDeviceContext* GetDeviceContext();
   nsresult GetRect(nsRect& aRect);
   nsresult GetAvailRect(nsRect& aRect);
+  nsresult GetWindowInnerRect(nsRect& aRect);
 
   mozilla::dom::ScreenOrientation mOrientation;
 
 private:
   class FullScreenEventListener final : public nsIDOMEventListener
   {
     ~FullScreenEventListener() {}
   public:
@@ -153,12 +154,14 @@ private:
     FULLSCREEN_LOCK_ALLOWED,
     LOCK_ALLOWED
   };
 
   LockPermission GetLockOrientationPermission() const;
 
   bool IsDeviceSizePageSize();
 
+  bool ShouldResistFingerprinting() const;
+
   nsRefPtr<FullScreenEventListener> mEventListener;
 };
 
 #endif /* nsScreen_h___ */
new file mode 100644
--- /dev/null
+++ b/dom/base/test/chrome/bug418986-1.js
@@ -0,0 +1,71 @@
+// The main test function.
+let test = function (isContent) {
+  SimpleTest.waitForExplicitFinish();
+
+  let { ww } = SpecialPowers.Services;
+  window.chromeWindow = ww.activeWindow;
+
+  // The pairs of values expected to be the same when
+  // fingerprinting resistance is enabled.
+  let pairs = [
+    ["screenX", 0],
+    ["screenY", 0],
+    ["mozInnerScreenX", 0],
+    ["mozInnerScreenY", 0],
+    ["screen.pixelDepth", 24],
+    ["screen.colorDepth", 24],
+    ["screen.availWidth", "innerWidth"],
+    ["screen.availHeight", "innerHeight"],
+    ["screen.left", 0],
+    ["screen.top", 0],
+    ["screen.availLeft", 0],
+    ["screen.availTop", 0],
+    ["screen.width", "innerWidth"],
+    ["screen.height", "innerHeight"],
+    ["screen.mozOrientation", "'landscape-primary'"],
+    ["devicePixelRatio", 1]
+  ];
+
+  // checkPair: tests if members of pair [a, b] are equal when evaluated.
+  let checkPair = function (a, b) {
+    is(eval(a), eval(b), a + " should be equal to " + b);
+  };
+
+  // Returns generator object that iterates through pref values.
+  let prefVals = (for (prefVal of [false, true]) prefVal);
+
+  // The main test function, runs until all pref values are exhausted.
+  let nextTest = function () {
+    let {value : prefValue, done} = prefVals.next();
+    if (done) {
+      SimpleTest.finish();
+      return;
+    }
+    SpecialPowers.pushPrefEnv({set : [["privacy.resistFingerprinting", prefValue]]},
+      function () {
+        // We will be resisting fingerprinting if the pref is enabled,
+        // and we are in a content script (not chrome).
+        let resisting = prefValue && isContent;
+        // Check each of the pairs.
+        pairs.map(function ([item, onVal]) {
+          if (resisting) {
+            checkPair("window." + item, onVal);
+          } else {
+            if (!item.startsWith("moz")) {
+              checkPair("window." + item, "chromeWindow." + item);
+            }
+          }
+        });
+        if (!resisting) {
+          // Hard to predict these values, but we can enforce constraints:
+          ok(window.mozInnerScreenX >= chromeWindow.mozInnerScreenX,
+             "mozInnerScreenX");
+          ok(window.mozInnerScreenY >= chromeWindow.mozInnerScreenY,
+             "mozInnerScreenY");
+        }
+      nextTest();
+    });
+  }
+
+  nextTest();
+}
--- a/dom/base/test/chrome/chrome.ini
+++ b/dom/base/test/chrome/chrome.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g'
 support-files =
   blockNoPlugins.xml
   blockPluginHard.xml
+  bug418986-1.js
   cpows_child.js
   cpows_parent.xul
   file_bug391728.html
   file_bug391728_2.html
   file_bug549682.xul
   file_bug616841.xul
   file_bug816340.xul
   file_bug990812-1.xul
@@ -26,16 +27,17 @@ support-files =
 
 [test_bug206691.xul]
 [test_bug339494.xul]
 [test_bug357450.xul]
 [test_bug380418.html]
 [test_bug380418.html^headers^]
 [test_bug383430.html]
 [test_bug391728.html]
+[test_bug418986-1.xul]
 [test_bug421622.xul]
 [test_bug429785.xul]
 [test_bug430050.xul]
 [test_bug467123.xul]
 [test_bug549682.xul]
 [test_bug571390.xul]
 [test_bug574596.html]
 [test_bug1098074_throw_from_ReceiveMessage.xul]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/chrome/test_bug418986-1.xul
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+                 type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=418986-1
+-->
+<window title="Mozilla Bug 418986 (Part 1)"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986-1"
+     target="_blank">Mozilla Bug 418986 (Part 1)</a>
+
+  <script type="application/javascript;version=1.7" src="bug418986-1.js"></script>
+  <!-- test code goes here -->
+  <script type="application/javascript"><![CDATA[
+    window.onload = function() {
+      test(false);
+    };
+  ]]></script>
+  </body>
+</window>
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -51,16 +51,17 @@ support-files =
   bug682592-subframe-ref.html
   bug682592-subframe.html
   bug696301-script-1.js
   bug696301-script-1.js^headers^
   bug696301-script-2.js
   bug704320.sjs
   bug704320_counter.sjs
   bug819051.sjs
+  chrome/bug418986-1.js
   copypaste.js
   delayedServerEvents.sjs
   echo.sjs
   eventsource.resource
   eventsource.resource^headers^
   eventsource_redirect.resource
   eventsource_redirect.resource^headers^
   eventsource_redirect_to.resource
@@ -448,16 +449,17 @@ support-files = test_bug402150.html^head
 [test_bug414190.html]
 [test_bug415860.html]
 [test_bug416317-1.html]
 [test_bug416317-2.html]
 [test_bug416383.html]
 [test_bug417255.html]
 [test_bug417384.html]
 [test_bug418214.html]
+[test_bug418986-1.html]
 [test_bug419132.html]
 [test_bug419527.xhtml]
 [test_bug420609.xhtml]
 [test_bug420700.html]
 [test_bug421602.html]
 [test_bug422403-1.html]
 skip-if = buildapp == 'b2g' # b2g(bug 901343, specialpowers.wrap issue [nsIChannel.open]) b2g-debug(bug 901343, specialpowers.wrap issue [nsIChannel.open]) b2g-desktop(bug 901343, specialpowers.wrap issue [nsIChannel.open])
 [test_bug422403-2.xhtml]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_bug418986-1.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=418986
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test 1/3 for Bug 418986 - Resist fingerprinting by preventing exposure of screen and system info</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.7" src="chrome/bug418986-1.js"></script>
+</head>
+<body>
+  <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986">Bug 418986</a>
+  <p id="display"></p>
+  <div id="content" style="display: none"></div>
+  <pre id="test"></pre>
+  <script>
+    window.onload = function() {
+      test(true);
+    };
+  </script>
+</body>
+</html>
--- a/dom/events/Event.cpp
+++ b/dom/events/Event.cpp
@@ -885,16 +885,23 @@ Event::Shutdown()
   }
 }
 
 LayoutDeviceIntPoint
 Event::GetScreenCoords(nsPresContext* aPresContext,
                        WidgetEvent* aEvent,
                        LayoutDeviceIntPoint aPoint)
 {
+  if (!nsContentUtils::IsCallerChrome() &&
+      nsContentUtils::ResistFingerprinting()) {
+    // When resisting fingerprinting, return client coordinates instead.
+    CSSIntPoint clientCoords = GetClientCoords(aPresContext, aEvent, aPoint, CSSIntPoint(0, 0));
+    return LayoutDeviceIntPoint(clientCoords.x, clientCoords.y);
+  }
+
   if (EventStateManager::sIsPointerLocked) {
     return EventStateManager::sLastScreenPoint;
   }
 
   if (!aEvent ||
        (aEvent->mClass != eMouseEventClass &&
         aEvent->mClass != eMouseScrollEventClass &&
         aEvent->mClass != eWheelEventClass &&
new file mode 100644
--- /dev/null
+++ b/dom/events/test/bug418986-3.js
@@ -0,0 +1,69 @@
+SimpleTest.waitForExplicitFinish();
+
+// The main testing function.
+let test = function (isContent) {
+  // Each definition is [eventType, prefSetting]
+  // Where we are setting the "privacy.resistFingerprinting" pref.
+  let eventDefs = [["mousedown", true],
+                   ["mouseup", true],
+                   ["mousedown", false],
+                   ["mouseup", false]];
+
+  let testCounter = 0;
+
+  // Declare ahead of time.
+  let setup;
+
+  // This function is called when the event handler fires.
+  let handleEvent = function (event, prefVal) {
+    let resisting = prefVal && isContent;
+    if (resisting) {
+      is(event.screenX, event.clientX, "event.screenX and event.clientX should be the same");
+      is(event.screenY, event.clientY, "event.screenY and event.clientY should be the same");
+    } else {
+      // We can't be sure about X coordinates not being equal, but we can test Y.
+      isnot(event.screenY, event.clientY, "event.screenY !== event.clientY");
+    }
+    ++testCounter;
+    if (testCounter < eventDefs.length) {
+      nextTest();
+    } else {
+      SimpleTest.finish();
+    }
+  };
+
+  // In this function, we set up the nth div and event handler,
+  // and then synthesize a mouse event in the div, to test
+  // whether the resulting events resist fingerprinting by
+  // suppressing absolute screen coordinates.
+  nextTest = function () {
+    let [eventType, prefVal] = eventDefs[testCounter];
+    SpecialPowers.pushPrefEnv({set:[["privacy.resistFingerprinting", prefVal]]},
+      function () {
+        // The following code creates a new div for each event in eventDefs,
+        // attaches a listener to listen for the event, and then generates
+        // a fake event at the center of the div.
+        let div = document.createElement("div");
+        div.style.width = "10px";
+        div.style.height = "10px";
+        div.style.backgroundColor = "red";
+        // Name the div after the event we're listening for.
+        div.id = eventType;
+        document.getElementById("body").appendChild(div);
+        // Seems we can't add an event listener in chrome unless we run
+        // it in a later task.
+        window.setTimeout(function() {
+          div.addEventListener(eventType, event => handleEvent(event, prefVal), false);
+          // For some reason, the following synthesizeMouseAtCenter call only seems to run if we
+          // wrap it in a window.setTimeout(..., 0).
+          window.setTimeout(function () {
+            synthesizeMouseAtCenter(div, {type : eventType});
+          }, 0);
+        }, 0);
+      });
+  };
+
+  // Now run by starting with the 0th event.
+  nextTest();
+
+};
--- a/dom/events/test/chrome.ini
+++ b/dom/events/test/chrome.ini
@@ -1,22 +1,24 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g'
 support-files =
   bug415498-doc1.html
   bug415498-doc2.html
+  bug418986-3.js
   bug591249_iframe.xul
   bug602962.xul
   file_bug679494.html
   window_bug617528.xul
   test_bug336682.js
 
 [test_bug336682_2.xul]
 [test_bug368835.html]
 [test_bug415498.xul]
+[test_bug418986-3.xul]
 [test_bug524674.xul]
 [test_bug586961.xul]
 [test_bug591249.xul]
 [test_bug602962.xul]
 [test_bug617528.xul]
 [test_bug679494.xul]
 [test_bug930374-chrome.html]
 [test_bug1128787-1.html]
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -2,16 +2,17 @@
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 support-files =
   bug226361_iframe.xhtml
   bug299673.js
   bug322588-popup.html
   bug426082.html
   bug648573.html
   bug656379-1.html
+  bug418986-3.js
   error_event_worker.js
   empty.js
   window_bug493251.html
   window_bug659071.html
   window_wheel_default_action.html
 
 [test_accel_virtual_modifier.html]
 [test_addEventListenerExtraArg.html]
@@ -33,16 +34,19 @@ support-files = test_bug336682.js
 [test_bug368835.html]
 [test_bug379120.html]
 [test_bug391568.xhtml]
 [test_bug402089.html]
 [test_bug405632.html]
 [test_bug409604.html]
 skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
 [test_bug412567.html]
+[test_bug418986-3.html]
+# Sometimes fails to finish after tests pass on 'B2G ICS Emulator'.
+skip-if = (os == 'b2g')
 [test_bug422132.html]
 skip-if = buildapp == 'b2g' || e10s # b2g(2 failures out of 8, mousewheel test) b2g-debug(2 failures out of 8, mousewheel test) b2g-desktop(2 failures out of 8, mousewheel test)
 [test_bug426082.html]
 skip-if = buildapp == 'mulet' || buildapp == 'b2g' || os == "win" || toolkit == 'android' || e10s # Intermittent failures, bug 921693 # b2g(1 failure out of 6, Moving the mouse down from the label should have unpressed the button) b2g-debug(1 failure out of 6, Moving the mouse down from the label should have unpressed the button) b2g-desktop(1 failure out of 6, Moving the mouse down from the label should have unpressed the button)
 [test_bug427537.html]
 [test_bug428988.html]
 [test_bug432698.html]
 skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_bug418986-3.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=418986
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test 3/3 for Bug 418986 - Resist fingerprinting by preventing exposure of screen and system info</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body id="body">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986">Bug 418986</a>
+<p id="display"></p>
+<pre id="test"></pre>
+<script type="application/javascript;version=1.7" src="bug418986-3.js"></script>
+<script type="application/javascript;version=1.7">
+  // This test produces fake mouse events and checks that the screenX and screenY
+  // properties of the received event objects provide client window coordinates.
+  // Run the test once the window has loaded.
+  window.onload = () => test(true);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/events/test/test_bug418986-3.xul
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+Bug 418986
+-->
+<window title="Mozilla Bug 418986"
+  xmlns:html="http://www.w3.org/1999/xhtml"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>      
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+
+<body id="body" xmlns="http://www.w3.org/1999/xhtml">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986">
+Mozilla Bug 418986</a>
+</body>
+
+<script type="application/javascript;version=1.7" src="bug418986-3.js"></script>
+<script type="application/javascript;version=1.7"><![CDATA[
+  // This test produces fake mouse events and checks that the screenX and screenY
+  // properties of the received event objects provide client window coordinates.
+  // Run the test once the window has loaded.
+  test(false);
+]]></script>  
+
+</window>
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -1445,16 +1445,20 @@ nsComputedDOMStyle::DoGetFontSizeAdjust(
   }
 
   return val;
 }
 
 CSSValue*
 nsComputedDOMStyle::DoGetOsxFontSmoothing()
 {
+  if (nsContentUtils::ShouldResistFingerprinting(
+        mPresShell->GetPresContext()->GetDocShell()))
+    return nullptr;
+
   nsROCSSPrimitiveValue* val = new nsROCSSPrimitiveValue;
   val->SetIdent(nsCSSProps::ValueToKeywordEnum(StyleFont()->mFont.smoothing,
                                                nsCSSProps::kFontSmoothingKTable));
   return val;
 }
 
 CSSValue*
 nsComputedDOMStyle::DoGetFontStretch()
--- a/layout/style/nsMediaFeatures.cpp
+++ b/layout/style/nsMediaFeatures.cpp
@@ -104,23 +104,29 @@ GetDeviceContextFor(nsPresContext* aPres
   // It would be nice to call
   // nsLayoutUtils::GetDeviceContextForScreenInfo here, except for two
   // things:  (1) it can flush, and flushing is bad here, and (2) it
   // doesn't really get us consistency in multi-monitor situations
   // *anyway*.
   return aPresContext->DeviceContext();
 }
 
+static bool
+ShouldResistFingerprinting(nsPresContext* aPresContext)
+{
+    return nsContentUtils::ShouldResistFingerprinting(aPresContext->GetDocShell());
+}
+
 // A helper for three features below.
 static nsSize
 GetDeviceSize(nsPresContext* aPresContext)
 {
     nsSize size;
 
-    if (aPresContext->IsDeviceSizePageSize()) {
+    if (ShouldResistFingerprinting(aPresContext) || aPresContext->IsDeviceSizePageSize()) {
         size = GetSize(aPresContext);
     } else if (aPresContext->IsRootPaginatedDocument()) {
         // We want the page size, including unprintable areas and margins.
         // XXX The spec actually says we want the "page sheet size", but
         // how is that different?
         size = aPresContext->GetPageSize();
     } else {
         GetDeviceContextFor(aPresContext)->
@@ -218,23 +224,27 @@ GetDeviceAspectRatio(nsPresContext* aPre
 {
     return MakeArray(GetDeviceSize(aPresContext), aResult);
 }
 
 static nsresult
 GetColor(nsPresContext* aPresContext, const nsMediaFeature*,
          nsCSSValue& aResult)
 {
-    // FIXME:  This implementation is bogus.  nsDeviceContext
-    // doesn't provide reliable information (should be fixed in bug
-    // 424386).
-    // FIXME: On a monochrome device, return 0!
-    nsDeviceContext *dx = GetDeviceContextFor(aPresContext);
-    uint32_t depth;
-    dx->GetDepth(depth);
+    uint32_t depth = 24; // Use depth of 24 when resisting fingerprinting.
+
+    if (!ShouldResistFingerprinting(aPresContext)) {
+        // FIXME:  This implementation is bogus.  nsDeviceContext
+        // doesn't provide reliable information (should be fixed in bug
+        // 424386).
+        // FIXME: On a monochrome device, return 0!
+        nsDeviceContext *dx = GetDeviceContextFor(aPresContext);
+        dx->GetDepth(depth);
+    }
+
     // The spec says to use bits *per color component*, so divide by 3,
     // and round down, since the spec says to use the smallest when the
     // color components differ.
     depth /= 3;
     aResult.SetIntValue(int32_t(depth), eCSSUnit_Integer);
     return NS_OK;
 }
 
@@ -262,20 +272,25 @@ GetMonochrome(nsPresContext* aPresContex
     aResult.SetIntValue(0, eCSSUnit_Integer);
     return NS_OK;
 }
 
 static nsresult
 GetResolution(nsPresContext* aPresContext, const nsMediaFeature*,
               nsCSSValue& aResult)
 {
-    // Resolution measures device pixels per CSS (inch/cm/pixel).  We
-    // return it in device pixels per CSS inches.
-    float dpi = float(nsPresContext::AppUnitsPerCSSInch()) /
-                float(aPresContext->AppUnitsPerDevPixel());
+    float dpi = 96; // Use 96 when resisting fingerprinting.
+
+    if (!ShouldResistFingerprinting(aPresContext)) {
+      // Resolution measures device pixels per CSS (inch/cm/pixel).  We
+      // return it in device pixels per CSS inches.
+      dpi = float(nsPresContext::AppUnitsPerCSSInch()) /
+            float(aPresContext->AppUnitsPerDevPixel());
+    }
+
     aResult.SetFloatValue(dpi, eCSSUnit_Inch);
     return NS_OK;
 }
 
 static nsresult
 GetScan(nsPresContext* aPresContext, const nsMediaFeature*,
         nsCSSValue& aResult)
 {
@@ -294,38 +309,53 @@ GetGrid(nsPresContext* aPresContext, con
     aResult.SetIntValue(0, eCSSUnit_Integer);
     return NS_OK;
 }
 
 static nsresult
 GetDevicePixelRatio(nsPresContext* aPresContext, const nsMediaFeature*,
                     nsCSSValue& aResult)
 {
-  float ratio = aPresContext->CSSPixelsToDevPixels(1.0f);
-  aResult.SetFloatValue(ratio, eCSSUnit_Number);
-  return NS_OK;
+    if (!ShouldResistFingerprinting(aPresContext)) {
+        float ratio = aPresContext->CSSPixelsToDevPixels(1.0f);
+        aResult.SetFloatValue(ratio, eCSSUnit_Number);
+    } else {
+        aResult.SetFloatValue(1.0, eCSSUnit_Number);
+    }
+    return NS_OK;
 }
 
 static nsresult
 GetSystemMetric(nsPresContext* aPresContext, const nsMediaFeature* aFeature,
                 nsCSSValue& aResult)
 {
+    aResult.Reset();
+    if (ShouldResistFingerprinting(aPresContext)) {
+        // If "privacy.resistFingerprinting" is enabled, then we simply don't
+        // return any system-backed media feature values. (No spoofed values returned.)
+        return NS_OK;
+    }
+
     MOZ_ASSERT(aFeature->mValueType == nsMediaFeature::eBoolInteger,
                "unexpected type");
     nsIAtom *metricAtom = *aFeature->mData.mMetric;
     bool hasMetric = nsCSSRuleProcessor::HasSystemMetric(metricAtom);
     aResult.SetIntValue(hasMetric ? 1 : 0, eCSSUnit_Integer);
     return NS_OK;
 }
 
 static nsresult
 GetWindowsTheme(nsPresContext* aPresContext, const nsMediaFeature* aFeature,
                 nsCSSValue& aResult)
 {
     aResult.Reset();
+    if (ShouldResistFingerprinting(aPresContext)) {
+        return NS_OK;
+    }
+
 #ifdef XP_WIN
     uint8_t windowsThemeId =
         nsCSSRuleProcessor::GetWindowsThemeIdentifier();
 
     // Classic mode should fail to match.
     if (windowsThemeId == LookAndFeel::eWindowsTheme_Classic)
         return NS_OK;
 
@@ -341,16 +371,20 @@ GetWindowsTheme(nsPresContext* aPresCont
     return NS_OK;
 }
 
 static nsresult
 GetOperatinSystemVersion(nsPresContext* aPresContext, const nsMediaFeature* aFeature,
                          nsCSSValue& aResult)
 {
     aResult.Reset();
+    if (ShouldResistFingerprinting(aPresContext)) {
+        return NS_OK;
+    }
+
 #ifdef XP_WIN
     int32_t metricResult;
     if (NS_SUCCEEDED(
           LookAndFeel::GetInt(LookAndFeel::eIntID_OperatingSystemVersionIdentifier,
                               &metricResult))) {
         for (size_t i = 0; i < ArrayLength(osVersionStrings); ++i) {
             if (metricResult == osVersionStrings[i].id) {
                 aResult.SetStringValue(nsDependentString(osVersionStrings[i].name),
new file mode 100644
--- /dev/null
+++ b/layout/style/test/chrome/bug418986-2.js
@@ -0,0 +1,274 @@
+// # Bug 418986, part 2.
+
+/* jshint esnext:true */
+/* jshint loopfunc:true */
+/* global window, screen, ok, SpecialPowers, matchMedia */
+
+SimpleTest.waitForExplicitFinish();
+
+// Expected values. Format: [name, pref_off_value, pref_on_value]
+// If pref_*_value is an array with two values, then we will match
+// any value in between those two values. If a value is null, then
+// we skip the media query.
+let expected_values = [
+  ["color", null, 8],
+  ["color-index", null, 0],
+  ["aspect-ratio", null, window.innerWidth + "/" + window.innerHeight],
+  ["device-aspect-ratio", screen.width + "/" + screen.height,
+                          window.innerWidth + "/" + window.innerHeight],
+  ["device-height", screen.height + "px", window.innerHeight + "px"],
+  ["device-width", screen.width + "px", window.innerWidth + "px"],
+  ["grid", null, 0],
+  ["height", window.innerHeight + "px", window.innerHeight + "px"],
+  ["monochrome", null, 0],
+  // Square is defined as portrait:
+  ["orientation", null,
+                  window.innerWidth > window.innerHeight ?
+                    "landscape" : "portrait"],
+  ["resolution", null, "96dpi"],
+  ["resolution", [0.999 * window.devicePixelRatio + "dppx",
+                  1.001 * window.devicePixelRatio + "dppx"], "1dppx"],
+  ["width", window.innerWidth + "px", window.innerWidth + "px"],
+  ["-moz-device-pixel-ratio", window.devicePixelRatio, 1],
+  ["-moz-device-orientation", screen.width > screen.height ?
+                                "landscape" : "portrait",
+                              window.innerWidth > window.innerHeight ?
+                                "landscape" : "portrait"]
+];
+
+// These media queries return value 0 or 1 when the pref is off.
+// When the pref is on, they should not match.
+let suppressed_toggles = [
+  "-moz-images-in-menus",
+  "-moz-mac-graphite-theme",
+  // Not available on most OSs.
+//  "-moz-maemo-classic",
+  "-moz-scrollbar-end-backward",
+  "-moz-scrollbar-end-forward",
+  "-moz-scrollbar-start-backward",
+  "-moz-scrollbar-start-forward",
+  "-moz-scrollbar-thumb-proportional",
+  "-moz-touch-enabled",
+  "-moz-windows-compositor",
+  "-moz-windows-default-theme",
+  "-moz-windows-glass",
+];
+
+// Possible values for '-moz-os-version'
+let windows_versions = [
+  "windows-xp",
+  "windows-vista",
+  "windows-win7",
+  "windows-win8"];
+
+// Possible values for '-moz-windows-theme'
+let windows_themes = [
+  "aero",
+  "luna-blue",
+  "luna-olive",
+  "luna-silver",
+  "royale",
+  "generic",
+  "zune"
+];
+
+// Read the current OS.
+let OS = SpecialPowers.Services.appinfo.OS;
+
+// If we are using Windows, add an extra toggle only
+// available on that OS.
+if (OS === "WINNT") {
+  suppressed_toggles.push("-moz-windows-classic");
+}
+
+// __keyValMatches(key, val)__.
+// Runs a media query and returns true if key matches to val.
+let keyValMatches = (key, val) => matchMedia("(" + key + ":" + val +")").matches;
+
+// __testMatch(key, val)__.
+// Attempts to run a media query match for the given key and value.
+// If value is an array of two elements [min max], then matches any
+// value in-between.
+let testMatch = function (key, val) {
+  if (val === null) {
+    return;
+  } else if (Array.isArray(val)) {
+    ok(keyValMatches("min-" + key, val[0]) && keyValMatches("max-" + key, val[1]),
+       "Expected " + key + " between " + val[0] + " and " + val[1]);
+  } else {
+    ok(keyValMatches(key, val), "Expected " + key + ":" + val);
+  }
+};
+
+// __testToggles(resisting)__.
+// Test whether we are able to match the "toggle" media queries.
+let testToggles = function (resisting) {
+  suppressed_toggles.forEach(
+    function (key) {
+      var exists = keyValMatches(key, 0) || keyValMatches(key, 1);
+      if (resisting) {
+         ok(!exists, key + " should not exist.");
+      } else {
+         ok(exists, key + " should exist.");
+      }
+    });
+};
+
+// __testWindowsSpecific__.
+// Runs a media query on the queryName with the given possible matching values.
+let testWindowsSpecific = function (resisting, queryName, possibleValues) {
+  let found = false;
+  possibleValues.forEach(function (val) {
+    found = found || keyValMatches(queryName, val);
+  });
+  if (resisting) {
+    ok(!found, queryName + " should have no match");
+  } else {
+    ok(found, queryName + " should match");
+  }
+};
+
+// __generateHtmlLines(resisting)__.
+// Create a series of div elements that look like:
+// `<div class='spoof' id='resolution'>resolution</div>`,
+// where each line corresponds to a different media query.
+let generateHtmlLines = function (resisting) {
+  let lines = "";
+  expected_values.forEach(
+    function ([key, offVal, onVal]) {
+      let val = resisting ? onVal : offVal;
+      if (val) {
+        lines += "<div class='spoof' id='" + key + "'>" + key + "</div>\n";
+      }
+    });
+  suppressed_toggles.forEach(
+    function (key) {
+      lines += "<div class='suppress' id='" + key + "'>" + key + "</div>\n";
+    });
+  if (OS === "WINNT") {
+    lines += "<div class='windows' id='-moz-os-version'>-moz-os-version</div>";
+    lines += "<div class='windows' id='-moz-windows-theme'>-moz-windows-theme</div>";
+  }
+  return lines;
+};
+
+// __cssLine__.
+// Creates a line of css that looks something like
+// `@media (resolution: 1ppx) { .spoof#resolution { background-color: green; } }`.
+let cssLine = function (query, clazz, id, color) {
+  return "@media " + query + " { ." + clazz +  "#" + id +
+         " { background-color: " + color + "; } }\n";
+};
+
+// __mediaQueryCSSLine(key, val, color)__.
+// Creates a line containing a CSS media query and a CSS expression.
+let mediaQueryCSSLine = function (key, val, color) {
+  if (val === null) {
+    return "";
+  }
+  let query;
+  if (Array.isArray(val)) {
+    query = "(min-" + key + ": " + val[0] + ") and (max-" +  key + ": " + val[1] + ")";
+  } else {
+    query = "(" + key + ": " + val + ")";
+  }
+  return cssLine(query, "spoof", key, color);
+};
+
+// __suppressedMediaQueryCSSLine(key, color)__.
+// Creates a CSS line that matches the existence of a
+// media query that is supposed to be suppressed.
+let suppressedMediaQueryCSSLine = function (key, color, suppressed) {
+  let query = "(" + key + ": 0), (" + key + ": 1)";
+  return cssLine(query, "suppress", key, color);
+};
+
+// __generateCSSLines(resisting)__.
+// Creates a series of lines of CSS, each of which corresponds to
+// a different media query. If the query produces a match to the
+// expected value, then the element will be colored green.
+let generateCSSLines = function (resisting) {
+  let lines = ".spoof { background-color: red;}\n";
+  expected_values.forEach(
+    function ([key, offVal, onVal]) {
+      lines += mediaQueryCSSLine(key, resisting ? onVal : offVal, "green");
+    });
+  lines += ".suppress { background-color: " + (resisting ? "green" : "red") + ";}\n";
+  suppressed_toggles.forEach(
+    function (key) {
+      lines += suppressedMediaQueryCSSLine(key, resisting ? "red" : "green");
+    });
+  if (OS === "WINNT") {
+    lines += ".windows { background-color: " + (resisting ? "green" : "red") + ";}\n";
+    lines += windows_versions.map(val => "(-moz-os-version: " + val + ")").join(", ") +
+             " { #-moz-os-version { background-color: " + (resisting ? "red" : "green") + ";} }\n";
+    lines += windows_themes.map(val => "(-moz-windows-theme: " + val + ")").join(",") +
+             " { #-moz-windows-theme { background-color: " + (resisting ? "red" : "green") + ";} }\n";
+  }
+  return lines;
+};
+
+// __green__.
+// Returns the computed color style corresponding to green.
+let green = (function () {
+  let temp = document.createElement("span");
+  temp.style.backgroundColor = "green";
+  return getComputedStyle(temp).backgroundColor;
+})();
+
+// __testCSS(resisting)__.
+// Creates a series of divs and CSS using media queries to set their
+// background color. If all media queries match as expected, then
+// all divs should have a green background color.
+let testCSS = function (resisting) {
+  document.getElementById("display").innerHTML = generateHtmlLines(resisting);
+  document.getElementById("test-css").innerHTML = generateCSSLines(resisting);
+  let cssTestDivs = document.querySelectorAll(".spoof,.suppress");
+  for (let div of cssTestDivs) {
+    let color = window.getComputedStyle(div).backgroundColor;
+    ok(color === green, "CSS for '" + div.id + "'");
+  }
+};
+
+// __testOSXFontSmoothing(resisting)__.
+// When fingerprinting resistance is enabled, the `getComputedStyle`
+// should always return `undefined` for `MozOSXFontSmoothing`.
+let testOSXFontSmoothing = function (resisting) {
+  let div = document.createElement("div");
+  div.style.MozOsxFontSmoothing = "unset";
+  let readBack = window.getComputedStyle(div).MozOsxFontSmoothing;
+  let smoothingPref = SpecialPowers.getBoolPref("layout.css.osx-font-smoothing.enabled", false);
+  is(readBack, resisting ? "" : (smoothingPref ? "auto" : ""),
+               "-moz-osx-font-smoothing");
+};
+
+// An iterator yielding pref values for two consecutive tests.
+let prefVals = (for (prefVal of [false, true]) prefVal);
+
+// __test(isContent)__.
+// Run all tests.
+let test = function(isContent) {
+  let {value: prefValue, done} = prefVals.next();
+  if (done) {
+    SimpleTest.finish();
+    return;
+  }
+  SpecialPowers.pushPrefEnv({set: [["privacy.resistFingerprinting", prefValue]]},
+    function () {
+      let resisting = prefValue && isContent;
+      expected_values.forEach(
+        function ([key, offVal, onVal]) {
+          testMatch(key, resisting ? onVal : offVal);
+        });
+      testToggles(resisting);
+      if (OS === "WINNT") {
+        testWindowsSpecific(resisting, "-moz-os-version", windows_versions);
+        testWindowsSpecific(resisting, "-moz-windows-theme", windows_themes);
+      }
+      testCSS(resisting);
+      if (OS === "Darwin") {
+        testOSXFontSmoothing(resisting);
+      }
+      test(isContent);
+    });
+};
--- a/layout/style/test/chrome/chrome.ini
+++ b/layout/style/test/chrome/chrome.ini
@@ -1,17 +1,19 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g'
 support-files =
+  bug418986-2.js
   bug535806-css.css
   bug535806-html.html
   bug535806-xul.xul
   hover_helper.html
 
 [test_addSheet.html]
 [test_additional_sheets.html]
 [test_author_specified_style.html]
+[test_bug418986-2.xul]
 [test_bug1157097.html]
 [test_bug1160724.xul]
 [test_bug535806.xul]
 [test_hover.html]
 skip-if = buildapp == 'mulet'
 [test_moz_document_rules.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/chrome/test_bug418986-2.xul
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=418986
+-->
+<window title="Mozilla Bug 418986"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <style id="test-css" scoped="true"></style>
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986"
+     target="_blank">Mozilla Bug 418986</a>
+  <p id="display"></p>
+
+  </body>
+
+  <script type="text/javascript;version=1.7" src="bug418986-2.js"></script>
+  <!-- test code goes here -->
+  <script type="text/javascript;version=1.7">
+    // Run all tests now.
+    window.onload = function () {
+      test(false);
+    };
+  </script>
+</window>
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 support-files =
   animation_utils.js
   ccd-quirks.html
   ccd.sjs
   ccd-standards.html
   css_properties.js
+  chrome/bug418986-2.js
   descriptor_database.js
   empty.html
   media_queries_dynamic_xbl_binding.xml
   media_queries_dynamic_xbl_iframe.html
   media_queries_dynamic_xbl_style.css
   media_queries_iframe.html
   neverending_font_load.sjs
   neverending_stylesheet_load.sjs
@@ -70,16 +71,17 @@ skip-if = toolkit == 'android'
 [test_bug397427.html]
 [test_bug399349.html]
 [test_bug401046.html]
 skip-if = true # Bug 701060
 [test_bug405818.html]
 [test_bug412901.html]
 skip-if = android_version == '18' # bug 1147986
 [test_bug413958.html]
+[test_bug418986-2.html]
 [test_bug437915.html]
 [test_bug450191.html]
 [test_bug453896_deck.html]
 support-files = bug453896_iframe.html
 [test_bug470769.html]
 [test_bug499655.html]
 [test_bug499655.xhtml]
 [test_bug511909.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_bug418986-2.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=418986
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test 2/3 for Bug #418986: Resist fingerprinting by preventing exposure of screen and system info</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <style id="test-css"></style>
+  <script type="text/javascript;version=1.7" src="chrome/bug418986-2.js"></script>
+  <script type="text/javascript;version=1.7">
+    // Run all tests now.
+    window.onload = function () {
+      test(true);
+    };
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986">Bug 418986</a>
+<p id="display">TEST</p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>