Explorar el Código

[1.8>1.9] Add 1ms jitter to Date.now and getMilliseconds

Both of these functions in Chakra use HiResTimer, so this changeset
copies the methodology used in Edge's implementation of time jitter
in a way that allows us to easily change the quantization frequency
if future mitigations require it.
Derek Morris hace 8 años
padre
commit
bfbcfa5b9e

+ 2 - 1
Build/Chakra.Build.props

@@ -78,7 +78,8 @@
   <PropertyGroup>
     <ChakraCommonLinkDependencies>
         oleaut32.lib;
-        version.lib
+        version.lib;
+	bcrypt.lib
     </ChakraCommonLinkDependencies>
     <RLCommonLinkDependencies>
         kernel32.lib;

+ 1 - 0
bin/ChakraCore/ChakraCore.vcxproj

@@ -50,6 +50,7 @@
         advapi32.lib;
         ole32.lib;
         Rpcrt4.lib;
+	bcrypt.lib;
         $(ChakraCommonLinkDependencies)
       </AdditionalDependencies>
       <AdditionalDependencies Condition="'$(IntlICU)'=='true'">

+ 1 - 0
bin/ch/ch.vcxproj

@@ -38,6 +38,7 @@
         kernel32.lib;
         Rpcrt4.lib;
         version.lib;
+	bcrypt.lib;
       </AdditionalDependencies>
     </Link>
   </ItemDefinitionGroup>

+ 158 - 3
lib/Runtime/PlatformAgnostic/Platform/Windows/HiResTimer.cpp

@@ -6,11 +6,163 @@
 #include "RuntimePlatformAgnosticPch.h"
 #include "Common.h"
 #include "ChakraPlatform.h"
+#include <Bcrypt.h>
 
 namespace PlatformAgnostic
 {
 namespace DateTime
 {
+    // Quantization code adapated from the version in Edge
+    template<uint64 frequencyOfQuantization>
+    class JitterManager
+    {
+        double frequencyOfSampling = 1.0;
+        double quantizationToSelectedScaleFactor = 1.0;
+        bool highFreq = false;
+        double currentRandomWindowScaled = 0.0;
+        ULONGLONG currentQuantizedQpc = 0;
+    public:
+        JitterManager::JitterManager()
+        {
+            // NOTE: We could cache the (1000/frequency) operation, as a double,
+            // that is used later to convert from seconds to milliseconds so that
+            // we don't keep recomputing it every time we need to convert from QPC
+            // to milliseconds (high-resolution only).  However, doing so would mean
+            // we have to worry about loss of precision that occurs through rounding
+            // and its propagation through any arithmetic operations subsequent to
+            // the conversion. Such loss of precision isn't necessarily significant,
+            // since the time values returned from CTimeManager aren't expected to be
+            // used in more than 1 or 2 subsequent arithmetic operations. The other
+            // potential source of precision loss occurs when a floating point value
+            // gets converted from a normal form to a denormal form, which only happens
+            // when trying to store a number smaller than 2^(-126), and we should never
+            // see a frequency big enough to cause that.  For example, worst case we would
+            // need a processor frequency of (1000/(2^(-126))=8.50705917*10^(31) GHz
+            // to go denormal.
+            LARGE_INTEGER tempFreq;
+            highFreq = QueryPerformanceFrequency(&tempFreq);
+            if (!highFreq)
+            {
+                // If we don't have a high-frequency source, the best we can do is GetSystemTime,
+                // which has a 1ms frequency.
+                frequencyOfSampling = 1000;
+            }
+            else
+            {
+                frequencyOfSampling = (double)tempFreq.QuadPart;
+            }
+
+            // frequency.QuadPart is the frequency of the QPC in counts per second.  For quantinization,
+            // we want to scale the QPC to units of counts per selected frequency.  We calculate the scale
+            // factor now:
+            //
+            //      e.g. 500us = 2000 counts per second
+            //
+            quantizationToSelectedScaleFactor = frequencyOfSampling / frequencyOfQuantization;
+
+            // If the scale factor is less than one, it means the hardware's QPC frequency is already
+            // the selected frequency or greater. In this case, we clamp to 1. This makes the arithmetic
+            // in QuantizeToFreq a no-op, which avoids losing further precision.
+            //
+            // We clamp to 1 rather than use an early exit to avoid untested blocks.
+            quantizationToSelectedScaleFactor = max(quantizationToSelectedScaleFactor, 1.0);
+        }
+
+        uint64 JitterManager::QuantizedQPC(uint64 qpc)
+        {
+            // Due to further analysis of some attacks, we're jittering on a more granular
+            // frequency of as much as a full millisecond.
+
+            // 'qpc' is currently in units of frequencyOfSampling counts per second.  We want to
+            // convert to units of frequencyOfQuantization, where sub-frequencyOfQuantization precision
+            // is expressed via the fractional part of a floating point number.
+            //
+            // We perform the conversion now, using the scale factor we precalculated in the
+            // constructor.
+            double preciseScaledResult = qpc / quantizationToSelectedScaleFactor;
+
+            // We now remove the fractional components, quantizing precision to the selected frequency by both ceiling and floor.
+            double ceilResult = ceil(preciseScaledResult);
+            double floorResult = floor(preciseScaledResult);
+
+            // Convert the results back to original QPC units, now at selected precision.
+            ceilResult *= quantizationToSelectedScaleFactor;
+            floorResult *= quantizationToSelectedScaleFactor;
+
+            // Convert these back to an integral value, taking the ceiling, even for the floored result.
+            // This will give us consistent results as the quantum moves (i.e. what is currently the
+            // quantizedQPC ends up being the floored QPC once we roll into the next window).
+            ULONGLONG quantizedQpc = static_cast<ULONGLONG>(ceil(ceilResult));
+            ULONGLONG quantizedQpcFloored = static_cast<ULONGLONG>(ceil(floorResult));
+
+            // The below converts the delta to milliseconds and checks that our quantized value does not
+            // diverge by more than our target quantization (plus an epsilon equal to 1 tick of the QPC).
+            AssertMsg(((quantizedQpc - qpc) * 1000.0 / frequencyOfSampling) <= (1000.0 / frequencyOfQuantization) + (1000.0 / frequencyOfSampling),
+                "W3C Timing: Difference between 'qpc' and 'quantizedQpc' expected to be <= selected frequency + epsilon.");
+
+            // If we're seeing this particular quantum for the first time, calculate a random portion of
+            // the beginning of the quantum in which we'll floor (and continue to report the previous quantum)
+            // instead of snapping to the next quantum.
+            // We don't do any of this quantiziation randomness if the scale factor is 1 (presumably because the
+            // QPC resolution was less than our selected quantum).
+            if (quantizationToSelectedScaleFactor != 1.0 && quantizedQpc != currentQuantizedQpc)
+            {
+                BYTE data[1];
+                NTSTATUS status = BCryptGenRandom(nullptr, data, sizeof(data), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
+                AssertOrFailFast(status == 0);
+                //Release_Assert(status == 0); IE does not have Release_Assert, but Chakra does.
+                if (BCRYPT_SUCCESS(status))
+                {
+                    // Convert the random byte to a double in the range [0.0, 1.0)
+                    // Note: if this ends up becoming performance critical, we can substitute the calculation with a
+                    // 2K lookup table (256 * 8) bytes.
+                    const double VALUES_IN_BYTE = 256.0;
+                    const double random0to1 = ((double)data[0] / VALUES_IN_BYTE);
+
+                    currentRandomWindowScaled = random0to1;
+                }
+                else
+                {
+                    currentRandomWindowScaled = (double)(rand() % 256) / 256.0;
+                }
+
+                // Record the fact that we've already generated the random range for this particular quantum.
+                // Note that this is not the reported one, but the actual quantum as calculated from the QPC.
+                currentQuantizedQpc = quantizedQpc;
+            }
+
+            // Grab off the fractional portion of the preciseScaledResult. As an example:
+            //   QueryPerformanceFrequency is 1,000,000
+            //   This means a QPC is 1us.
+            //   Quantum of 1000us means a QPC of 125 would result in a fractional portion of 0.125
+            //   (math works out to (125 % 1000) / 1000).
+            // This fractional portion is then evaluated in order to determine whether or not to snap
+            // forward and use the next quantum, or use the floored one. This calculation gives us random
+            // windows in which specific 1000us timestamps are observed for a non-deterministic amount of time.
+            double preciseScaledFractional = preciseScaledResult - ((ULONGLONG)preciseScaledResult);
+            if (preciseScaledFractional < currentRandomWindowScaled)
+            {
+                quantizedQpc = quantizedQpcFloored;
+            }
+
+            return quantizedQpc;
+        }
+    };
+
+    // Set to jitter to an accuracy of 1000 ticks/second or 1ms
+    static thread_local JitterManager<1000> timeJitter;
+
+    double GetQuantizedSystemTime()
+    {
+        SYSTEMTIME stTime;
+        ::GetSystemTime(&stTime);
+        // In the event that we have no high-res timers available, we don't have the needed
+        // granularity to jitter at a 1ms precision. We need to do something (as otherwise,
+        // the timer precision is higher than on a high-precision machine), but the best we
+        // can do is likely to strongly reduce the timer accuracy. Here we group by 4ms.
+        stTime.wMilliseconds = (stTime.wMilliseconds / 4) * 4;
+        return Js::DateUtilities::TimeFromSt(&stTime);
+    }
 
     double HiResTimer::GetSystemTime()
     {
@@ -38,7 +190,7 @@ namespace DateTime
     {
         if(!data.fHiResAvailable)
         {
-            return GetSystemTime();
+            return GetQuantizedSystemTime();
         }
 
         if(!data.fInit)
@@ -46,7 +198,7 @@ namespace DateTime
             if (!QueryPerformanceFrequency((LARGE_INTEGER *) &(data.freq)))
             {
                 data.fHiResAvailable = false;
-                return GetSystemTime();
+                return GetQuantizedSystemTime();
             }
             data.fInit = true;
         }
@@ -60,11 +212,14 @@ namespace DateTime
         if( !QueryPerformanceCounter((LARGE_INTEGER *) &count))
         {
             data.fHiResAvailable = false;
-            return GetSystemTime();
+            return GetQuantizedSystemTime();
         }
 
         double time = GetSystemTime();
 
+        // quantize the timestamp to hinder timing attacks
+        count = timeJitter.QuantizedQPC(count);
+
         // there is a base time and count set.
         if (!data.fReset
             && (count >= data.baseMsCount)) // Make sure we don't regress