Bug 1369309 - Part 3: Making the media statistics reports a spoofed value when fingerprinting resistance is enabled. r?cpearce,arthuredelstein draft
authorTim Huang <tihuang@mozilla.com>
Mon, 17 Jul 2017 15:13:55 +0800
changeset 643930 beda8f5d86c01c67c506e43be2953321eda34f92
parent 643929 ecbdb23c65eb1f18df3177b944964cda9af77a4f
child 643931 1d93798772423c01218c7b2103ba5e3e9e2fc506
push id73260
push userbmo:tihuang@mozilla.com
push dateThu, 10 Aug 2017 08:38:35 +0000
reviewerscpearce, arthuredelstein
bugs1369309
milestone57.0a1
Bug 1369309 - Part 3: Making the media statistics reports a spoofed value when fingerprinting resistance is enabled. r?cpearce,arthuredelstein This patch makes the media statistics report values with a fixed frames per second and a dynamic dropped ratio when resistance fingerprinting is enabled. The dropped rate is decided by the video resolution that it will report a fixed dropped rate when the video resolution is greater than 480p. And It will report a zero dropped rate if the video is below or equal to 480p. In addition, it adds three new prefs that allow us to change the value of frames per second, the dropped ratio and the threshold of target video resolution. The three prefs are 'privacy.resistFingerprinting.video_frames_per_sec', 'privacy.resistFingerprinting.video_dropped_ratio' and 'privacy.resistFingerprinting.target_video_res'. The default values of them are 30, 5 and 480, which means 30 frames per second, 5 percent dropped ratio and 480p. This also adds a new helper function 'nsContentUtils::ShouldResistFingerprinting(nsIDocument* aDoc)' for checking whether fingerprinting resistance is enabled for a given docuemnt. If it is a chrome document, this function will indicate that fingerprinting resistance is not enabled regardless of the pref 'privacy.resistFingerprinting'. If it is a content document, the result will depend on the pref. MozReview-Commit-ID: FbSuRq6Zdnn
dom/base/nsContentUtils.cpp
dom/base/nsContentUtils.h
dom/html/HTMLVideoElement.cpp
dom/html/HTMLVideoElement.h
toolkit/components/resistfingerprinting/nsRFPService.cpp
toolkit/components/resistfingerprinting/nsRFPService.h
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -2400,17 +2400,26 @@ nsContentUtils::ShouldResistFingerprinti
 }
 
 bool
 nsContentUtils::ShouldResistFingerprinting(nsIDocShell* aDocShell)
 {
   if (!aDocShell) {
     return false;
   }
-  bool isChrome = nsContentUtils::IsChromeDoc(aDocShell->GetDocument());
+  return ShouldResistFingerprinting(aDocShell->GetDocument());
+}
+
+/* static */
+bool
+nsContentUtils::ShouldResistFingerprinting(nsIDocument* aDoc) {
+  if (!aDoc) {
+    return false;
+  }
+  bool isChrome = nsContentUtils::IsChromeDoc(aDoc);
   return !isChrome && ShouldResistFingerprinting();
 }
 
 /* static */
 void
 nsContentUtils::CalcRoundedWindowSizeForResistingFingerprinting(int32_t  aChromeWidth,
                                                                 int32_t  aChromeHeight,
                                                                 int32_t  aScreenWidth,
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -271,16 +271,17 @@ public:
   static bool LookupBindingMember(JSContext* aCx, nsIContent *aContent,
                                   JS::Handle<jsid> aId,
                                   JS::MutableHandle<JS::PropertyDescriptor> aDesc);
 
   // Check whether we should avoid leaking distinguishing information to JS/CSS.
   // This function can be called both in the main thread and worker threads.
   static bool ShouldResistFingerprinting();
   static bool ShouldResistFingerprinting(nsIDocShell* aDocShell);
+  static bool ShouldResistFingerprinting(nsIDocument* aDoc);
 
   // A helper function to calculate the rounded window size for fingerprinting
   // resistance. The rounded size is based on the chrome UI size and available
   // screen size. If the inputWidth/Height is greater than the available content
   // size, this will report the available content size. Otherwise, it will
   // round the size to the nearest upper 200x100.
   static void CalcRoundedWindowSizeForResistingFingerprinting(int32_t  aChromeWidth,
                                                               int32_t  aChromeHeight,
--- a/dom/html/HTMLVideoElement.cpp
+++ b/dom/html/HTMLVideoElement.cpp
@@ -149,51 +149,73 @@ HTMLVideoElement::IsInteractiveHTMLConte
 }
 
 uint32_t HTMLVideoElement::MozParsedFrames() const
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
   if (!IsVideoStatsEnabled()) {
     return 0;
   }
+
+  if (nsContentUtils::ShouldResistFingerprinting(OwnerDoc())) {
+    return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
+  }
+
   return mDecoder ? mDecoder->GetFrameStatistics().GetParsedFrames() : 0;
 }
 
 uint32_t HTMLVideoElement::MozDecodedFrames() const
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
   if (!IsVideoStatsEnabled()) {
     return 0;
   }
+
+  if (nsContentUtils::ShouldResistFingerprinting(OwnerDoc())) {
+    return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
+  }
+
   return mDecoder ? mDecoder->GetFrameStatistics().GetDecodedFrames() : 0;
 }
 
 uint32_t HTMLVideoElement::MozPresentedFrames() const
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
   if (!IsVideoStatsEnabled()) {
     return 0;
   }
+
+  if (nsContentUtils::ShouldResistFingerprinting(OwnerDoc())) {
+    return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(), VideoWidth(), VideoHeight());
+  }
+
   return mDecoder ? mDecoder->GetFrameStatistics().GetPresentedFrames() : 0;
 }
 
 uint32_t HTMLVideoElement::MozPaintedFrames()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
   if (!IsVideoStatsEnabled()) {
     return 0;
   }
+
+  if (nsContentUtils::ShouldResistFingerprinting(OwnerDoc())) {
+    return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(), VideoWidth(), VideoHeight());
+  }
+
   layers::ImageContainer* container = GetImageContainer();
   return container ? container->GetPaintCount() : 0;
 }
 
 double HTMLVideoElement::MozFrameDelay()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
-  if (!IsVideoStatsEnabled()) {
+
+  if (!IsVideoStatsEnabled() ||
+      nsContentUtils::ShouldResistFingerprinting(OwnerDoc())) {
     return 0.0;
   }
 
   VideoFrameContainer* container = GetVideoFrameContainer();
   // Hide negative delays. Frame timing tweaks in the compositor (e.g.
   // adding a bias value to prevent multiple dropped/duped frames when
   // frame times are aligned with composition times) may produce apparent
   // negative delay, but we shouldn't report that.
@@ -250,35 +272,43 @@ HTMLVideoElement::GetVideoPlaybackQualit
     if (nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow()) {
       Performance* perf = window->GetPerformance();
       if (perf) {
         creationTime = perf->Now();
       }
     }
 
     if (mDecoder) {
-      FrameStatisticsData stats =
-        mDecoder->GetFrameStatistics().GetFrameStatisticsData();
-      if (sizeof(totalFrames) >= sizeof(stats.mParsedFrames)) {
-        totalFrames = stats.mPresentedFrames + stats.mDroppedFrames;
-        droppedFrames = stats.mDroppedFrames;
+      if (nsContentUtils::ShouldResistFingerprinting(OwnerDoc())) {
+        totalFrames = nsRFPService::GetSpoofedTotalFrames(TotalPlayTime());
+        droppedFrames = nsRFPService::GetSpoofedDroppedFrames(TotalPlayTime(),
+                                                              VideoWidth(),
+                                                              VideoHeight());
+        corruptedFrames = 0;
       } else {
-        uint64_t total = stats.mPresentedFrames + stats.mDroppedFrames;
-        const auto maxNumber = std::numeric_limits<uint32_t>::max();
-        if (total <= maxNumber) {
-          totalFrames = uint32_t(total);
-          droppedFrames = uint32_t(stats.mDroppedFrames);
+        FrameStatisticsData stats =
+          mDecoder->GetFrameStatistics().GetFrameStatisticsData();
+        if (sizeof(totalFrames) >= sizeof(stats.mParsedFrames)) {
+          totalFrames = stats.mPresentedFrames + stats.mDroppedFrames;
+          droppedFrames = stats.mDroppedFrames;
         } else {
-          // Too big number(s) -> Resize everything to fit in 32 bits.
-          double ratio = double(maxNumber) / double(total);
-          totalFrames = maxNumber; // === total * ratio
-          droppedFrames = uint32_t(double(stats.mDroppedFrames) * ratio);
+          uint64_t total = stats.mPresentedFrames + stats.mDroppedFrames;
+          const auto maxNumber = std::numeric_limits<uint32_t>::max();
+          if (total <= maxNumber) {
+            totalFrames = uint32_t(total);
+            droppedFrames = uint32_t(stats.mDroppedFrames);
+          } else {
+            // Too big number(s) -> Resize everything to fit in 32 bits.
+            double ratio = double(maxNumber) / double(total);
+            totalFrames = maxNumber; // === total * ratio
+            droppedFrames = uint32_t(double(stats.mDroppedFrames) * ratio);
+          }
         }
+        corruptedFrames = 0;
       }
-      corruptedFrames = 0;
     }
   }
 
   RefPtr<VideoPlaybackQuality> playbackQuality =
     new VideoPlaybackQuality(this, creationTime, totalFrames, droppedFrames,
                              corruptedFrames);
   return playbackQuality.forget();
 }
@@ -326,14 +356,43 @@ HTMLVideoElement::UpdateScreenWakeLock()
 void
 HTMLVideoElement::Init()
 {
   Preferences::AddBoolVarCache(&sVideoStatsEnabled, "media.video_stats.enabled");
 }
 
 /* static */
 bool
-HTMLVideoElement::IsVideoStatsEnabled() {
-  return sVideoStatsEnabled && !nsContentUtils::ShouldResistFingerprinting();
+HTMLVideoElement::IsVideoStatsEnabled()
+{
+  return sVideoStatsEnabled;
+}
+
+double
+HTMLVideoElement::TotalPlayTime() const
+{
+  double total = 0.0;
+
+  if (mPlayed) {
+    uint32_t timeRangeCount = 0;
+    mPlayed->GetLength(&timeRangeCount);
+
+    for (uint32_t i = 0; i < timeRangeCount; i++) {
+      double begin;
+      double end;
+      mPlayed->Start(i, &begin);
+      mPlayed->End(i, &end);
+      total += end - begin;
+    }
+
+    if (mCurrentPlayRangeStart != -1.0) {
+      double now = CurrentTime();
+      if (mCurrentPlayRangeStart != now) {
+        total += now - mCurrentPlayRangeStart;
+      }
+    }
+  }
+
+  return total;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/html/HTMLVideoElement.h
+++ b/dom/html/HTMLVideoElement.h
@@ -148,14 +148,15 @@ protected:
   bool mUseScreenWakeLock;
   RefPtr<WakeLock> mScreenWakeLock;
 
 private:
   static void MapAttributesIntoRule(const nsMappedAttributes* aAttributes,
                                     GenericSpecifiedValues* aGenericData);
 
   static bool IsVideoStatsEnabled();
+  double TotalPlayTime() const;
 };
 
 } // namespace dom
 } // namespace mozilla
 
 #endif // mozilla_dom_HTMLVideoElement_h
--- a/toolkit/components/resistfingerprinting/nsRFPService.cpp
+++ b/toolkit/components/resistfingerprinting/nsRFPService.cpp
@@ -1,47 +1,59 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsRFPService.h"
 
+#include <algorithm>
 #include <time.h>
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 
 #include "nsCOMPtr.h"
+#include "nsCoord.h"
 #include "nsServiceManagerUtils.h"
 #include "nsString.h"
 #include "nsXULAppAPI.h"
 
 #include "nsIObserverService.h"
 #include "nsIPrefBranch.h"
 #include "nsIPrefService.h"
 #include "nsJSUtils.h"
 
 #include "prenv.h"
 
 #include "js/Date.h"
 
 using namespace mozilla;
+using namespace std;
 
 #define RESIST_FINGERPRINTING_PREF "privacy.resistFingerprinting"
+#define RFP_SPOOFED_FRAMES_PER_SEC_PREF "privacy.resistFingerprinting.video_frames_per_sec"
+#define RFP_SPOOFED_DROPPED_RATIO_PREF  "privacy.resistFingerprinting.video_dropped_ratio"
+#define RFP_TARGET_VIDEO_RES_PREF "privacy.resistFingerprinting.target_video_res"
+#define RFP_SPOOFED_FRAMES_PER_SEC_DEFAULT 30
+#define RFP_SPOOFED_DROPPED_RATIO_DEFAULT  5
+#define RFP_TARGET_VIDEO_RES_DEFAULT 480
 #define PROFILE_INITIALIZED_TOPIC "profile-initial-state"
 
 NS_IMPL_ISUPPORTS(nsRFPService, nsIObserver)
 
 static StaticRefPtr<nsRFPService> sRFPService;
 static bool sInitialized = false;
 Atomic<bool, ReleaseAcquire> nsRFPService::sPrivacyResistFingerprinting;
 static uint32_t kResolutionUSec = 100000;
+static uint32_t sVideoFramesPerSec;
+static uint32_t sVideoDroppedRatio;
+static uint32_t sTargetVideoRes;
 
 /* static */
 nsRFPService*
 nsRFPService::GetOrCreate()
 {
   if (!sInitialized) {
     sRFPService = new nsRFPService();
     nsresult rv = sRFPService->Init();
@@ -75,32 +87,86 @@ nsRFPService::ReduceTimePrecisionAsUSecs
 {
   if (!IsResistFingerprintingEnabled()) {
     return aTime;
   }
   return floor(aTime / kResolutionUSec) * kResolutionUSec;
 }
 
 /* static */
+uint32_t
+nsRFPService::CalculateTargetVideoResolution(uint32_t aVideoQuality)
+{
+  return aVideoQuality * NSToIntCeil(aVideoQuality * 16 / 9.0);
+}
+
+/* static */
 double
 nsRFPService::ReduceTimePrecisionAsSecs(double aTime)
 {
   if (!IsResistFingerprintingEnabled()) {
     return aTime;
   }
   if (kResolutionUSec < 1000000) {
     // The resolution is smaller than one sec.  Use the reciprocal to avoid
     // floating point error.
     const double resolutionSecReciprocal = 1000000.0 / kResolutionUSec;
     return floor(aTime * resolutionSecReciprocal) / resolutionSecReciprocal;
   }
   const double resolutionSec = kResolutionUSec / 1000000.0;
   return floor(aTime / resolutionSec) * resolutionSec;
 }
 
+/* static */
+uint32_t
+nsRFPService::GetSpoofedTotalFrames(double aTime)
+{
+  double time = ReduceTimePrecisionAsSecs(aTime);
+
+  return NSToIntFloor(time * sVideoFramesPerSec);
+}
+
+/* static */
+uint32_t
+nsRFPService::GetSpoofedDroppedFrames(double aTime, uint32_t aWidth, uint32_t aHeight)
+{
+  uint32_t targetRes = CalculateTargetVideoResolution(sTargetVideoRes);
+
+  // The video resolution is less than or equal to the target resolution, we
+  // report a zero dropped rate for this case.
+  if (targetRes >= aWidth * aHeight) {
+    return 0;
+  }
+
+  double time = ReduceTimePrecisionAsSecs(aTime);
+  // Bound the dropped ratio from 0 to 100.
+  uint32_t boundedDroppedRatio = min(sVideoDroppedRatio, 100u);
+
+  return NSToIntFloor(time * sVideoFramesPerSec * (boundedDroppedRatio / 100.0));
+}
+
+/* static */
+uint32_t
+nsRFPService::GetSpoofedPresentedFrames(double aTime, uint32_t aWidth, uint32_t aHeight)
+{
+  uint32_t targetRes = CalculateTargetVideoResolution(sTargetVideoRes);
+
+  // The target resolution is greater than the current resolution. For this case,
+  // there will be no dropped frames, so we report total frames directly.
+  if (targetRes >= aWidth * aHeight) {
+    return GetSpoofedTotalFrames(aTime);
+  }
+
+  double time = ReduceTimePrecisionAsSecs(aTime);
+  // Bound the dropped ratio from 0 to 100.
+  uint32_t boundedDroppedRatio = min(sVideoDroppedRatio, 100u);
+
+  return NSToIntFloor(time * sVideoFramesPerSec * ((100 - boundedDroppedRatio) / 100.0));
+}
+
 nsresult
 nsRFPService::Init()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   nsresult rv;
 
   nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
@@ -115,16 +181,26 @@ nsRFPService::Init()
 #endif
 
   nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
   NS_ENSURE_TRUE(prefs, NS_ERROR_NOT_AVAILABLE);
 
   rv = prefs->AddObserver(RESIST_FINGERPRINTING_PREF, this, false);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  Preferences::AddUintVarCache(&sVideoFramesPerSec,
+                               RFP_SPOOFED_FRAMES_PER_SEC_PREF,
+                               RFP_SPOOFED_FRAMES_PER_SEC_DEFAULT);
+  Preferences::AddUintVarCache(&sVideoDroppedRatio,
+                               RFP_SPOOFED_DROPPED_RATIO_PREF,
+                               RFP_SPOOFED_DROPPED_RATIO_DEFAULT);
+  Preferences::AddUintVarCache(&sTargetVideoRes,
+                               RFP_TARGET_VIDEO_RES_PREF,
+                               RFP_TARGET_VIDEO_RES_DEFAULT);
+
   // We backup the original TZ value here.
   const char* tzValue = PR_GetEnv("TZ");
   if (tzValue) {
     mInitialTZValue = nsCString(tzValue);
   }
 
   // Call UpdatePref() here to cache the value of 'privacy.resistFingerprinting'
   // and set the timezone.
--- a/toolkit/components/resistfingerprinting/nsRFPService.h
+++ b/toolkit/components/resistfingerprinting/nsRFPService.h
@@ -34,16 +34,26 @@ public:
     return sPrivacyResistFingerprinting;
   }
 
   // The following Reduce methods can be called off main thread.
   static double ReduceTimePrecisionAsMSecs(double aTime);
   static double ReduceTimePrecisionAsUSecs(double aTime);
   static double ReduceTimePrecisionAsSecs(double aTime);
 
+  // This method calculates the video resolution (i.e. height x width) based
+  // on the video quality (480p, 720p, etc).
+  static uint32_t CalculateTargetVideoResolution(uint32_t aVideoQuality);
+
+  // Methods for getting spoofed media statistics and the return value will
+  // depend on the video resolution.
+  static uint32_t GetSpoofedTotalFrames(double aTime);
+  static uint32_t GetSpoofedDroppedFrames(double aTime, uint32_t aWidth, uint32_t aHeight);
+  static uint32_t GetSpoofedPresentedFrames(double aTime, uint32_t aWidth, uint32_t aHeight);
+
 private:
   nsresult Init();
 
   nsRFPService() {}
 
   ~nsRFPService() {}
 
   void UpdatePref();