|
|
@@ -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
|