Skip to content

Commit 7254857

Browse files
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.
1 parent 181ade5 commit 7254857

File tree

4 files changed

+162
-4
lines changed

4 files changed

+162
-4
lines changed

Build/Chakra.Build.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@
7878
<PropertyGroup>
7979
<ChakraCommonLinkDependencies>
8080
oleaut32.lib;
81-
version.lib
81+
version.lib;
82+
bcrypt.lib
8283
</ChakraCommonLinkDependencies>
8384
<RLCommonLinkDependencies>
8485
kernel32.lib;

bin/ChakraCore/ChakraCore.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
advapi32.lib;
5151
ole32.lib;
5252
Rpcrt4.lib;
53+
bcrypt.lib;
5354
$(ChakraCommonLinkDependencies)
5455
</AdditionalDependencies>
5556
<AdditionalDependencies Condition="'$(IntlICU)'=='true'">

bin/ch/ch.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
kernel32.lib;
3939
Rpcrt4.lib;
4040
version.lib;
41+
bcrypt.lib;
4142
</AdditionalDependencies>
4243
</Link>
4344
</ItemDefinitionGroup>

lib/Runtime/PlatformAgnostic/Platform/Windows/HiResTimer.cpp

Lines changed: 158 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,163 @@
66
#include "RuntimePlatformAgnosticPch.h"
77
#include "Common.h"
88
#include "ChakraPlatform.h"
9+
#include <Bcrypt.h>
910

1011
namespace PlatformAgnostic
1112
{
1213
namespace DateTime
1314
{
15+
// Quantization code adapated from the version in Edge
16+
template<uint64 frequencyOfQuantization>
17+
class JitterManager
18+
{
19+
double frequencyOfSampling = 1.0;
20+
double quantizationToSelectedScaleFactor = 1.0;
21+
bool highFreq = false;
22+
double currentRandomWindowScaled = 0.0;
23+
ULONGLONG currentQuantizedQpc = 0;
24+
public:
25+
JitterManager::JitterManager()
26+
{
27+
// NOTE: We could cache the (1000/frequency) operation, as a double,
28+
// that is used later to convert from seconds to milliseconds so that
29+
// we don't keep recomputing it every time we need to convert from QPC
30+
// to milliseconds (high-resolution only). However, doing so would mean
31+
// we have to worry about loss of precision that occurs through rounding
32+
// and its propagation through any arithmetic operations subsequent to
33+
// the conversion. Such loss of precision isn't necessarily significant,
34+
// since the time values returned from CTimeManager aren't expected to be
35+
// used in more than 1 or 2 subsequent arithmetic operations. The other
36+
// potential source of precision loss occurs when a floating point value
37+
// gets converted from a normal form to a denormal form, which only happens
38+
// when trying to store a number smaller than 2^(-126), and we should never
39+
// see a frequency big enough to cause that. For example, worst case we would
40+
// need a processor frequency of (1000/(2^(-126))=8.50705917*10^(31) GHz
41+
// to go denormal.
42+
LARGE_INTEGER tempFreq;
43+
highFreq = QueryPerformanceFrequency(&tempFreq);
44+
if (!highFreq)
45+
{
46+
// If we don't have a high-frequency source, the best we can do is GetSystemTime,
47+
// which has a 1ms frequency.
48+
frequencyOfSampling = 1000;
49+
}
50+
else
51+
{
52+
frequencyOfSampling = (double)tempFreq.QuadPart;
53+
}
54+
55+
// frequency.QuadPart is the frequency of the QPC in counts per second. For quantinization,
56+
// we want to scale the QPC to units of counts per selected frequency. We calculate the scale
57+
// factor now:
58+
//
59+
// e.g. 500us = 2000 counts per second
60+
//
61+
quantizationToSelectedScaleFactor = frequencyOfSampling / frequencyOfQuantization;
62+
63+
// If the scale factor is less than one, it means the hardware's QPC frequency is already
64+
// the selected frequency or greater. In this case, we clamp to 1. This makes the arithmetic
65+
// in QuantizeToFreq a no-op, which avoids losing further precision.
66+
//
67+
// We clamp to 1 rather than use an early exit to avoid untested blocks.
68+
quantizationToSelectedScaleFactor = max(quantizationToSelectedScaleFactor, 1.0);
69+
}
70+
71+
uint64 JitterManager::QuantizedQPC(uint64 qpc)
72+
{
73+
// Due to further analysis of some attacks, we're jittering on a more granular
74+
// frequency of as much as a full millisecond.
75+
76+
// 'qpc' is currently in units of frequencyOfSampling counts per second. We want to
77+
// convert to units of frequencyOfQuantization, where sub-frequencyOfQuantization precision
78+
// is expressed via the fractional part of a floating point number.
79+
//
80+
// We perform the conversion now, using the scale factor we precalculated in the
81+
// constructor.
82+
double preciseScaledResult = qpc / quantizationToSelectedScaleFactor;
83+
84+
// We now remove the fractional components, quantizing precision to the selected frequency by both ceiling and floor.
85+
double ceilResult = ceil(preciseScaledResult);
86+
double floorResult = floor(preciseScaledResult);
87+
88+
// Convert the results back to original QPC units, now at selected precision.
89+
ceilResult *= quantizationToSelectedScaleFactor;
90+
floorResult *= quantizationToSelectedScaleFactor;
91+
92+
// Convert these back to an integral value, taking the ceiling, even for the floored result.
93+
// This will give us consistent results as the quantum moves (i.e. what is currently the
94+
// quantizedQPC ends up being the floored QPC once we roll into the next window).
95+
ULONGLONG quantizedQpc = static_cast<ULONGLONG>(ceil(ceilResult));
96+
ULONGLONG quantizedQpcFloored = static_cast<ULONGLONG>(ceil(floorResult));
97+
98+
// The below converts the delta to milliseconds and checks that our quantized value does not
99+
// diverge by more than our target quantization (plus an epsilon equal to 1 tick of the QPC).
100+
AssertMsg(((quantizedQpc - qpc) * 1000.0 / frequencyOfSampling) <= (1000.0 / frequencyOfQuantization) + (1000.0 / frequencyOfSampling),
101+
"W3C Timing: Difference between 'qpc' and 'quantizedQpc' expected to be <= selected frequency + epsilon.");
102+
103+
// If we're seeing this particular quantum for the first time, calculate a random portion of
104+
// the beginning of the quantum in which we'll floor (and continue to report the previous quantum)
105+
// instead of snapping to the next quantum.
106+
// We don't do any of this quantiziation randomness if the scale factor is 1 (presumably because the
107+
// QPC resolution was less than our selected quantum).
108+
if (quantizationToSelectedScaleFactor != 1.0 && quantizedQpc != currentQuantizedQpc)
109+
{
110+
BYTE data[1];
111+
NTSTATUS status = BCryptGenRandom(nullptr, data, sizeof(data), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
112+
AssertOrFailFast(status == 0);
113+
//Release_Assert(status == 0); IE does not have Release_Assert, but Chakra does.
114+
if (BCRYPT_SUCCESS(status))
115+
{
116+
// Convert the random byte to a double in the range [0.0, 1.0)
117+
// Note: if this ends up becoming performance critical, we can substitute the calculation with a
118+
// 2K lookup table (256 * 8) bytes.
119+
const double VALUES_IN_BYTE = 256.0;
120+
const double random0to1 = ((double)data[0] / VALUES_IN_BYTE);
121+
122+
currentRandomWindowScaled = random0to1;
123+
}
124+
else
125+
{
126+
currentRandomWindowScaled = (double)(rand() % 256) / 256.0;
127+
}
128+
129+
// Record the fact that we've already generated the random range for this particular quantum.
130+
// Note that this is not the reported one, but the actual quantum as calculated from the QPC.
131+
currentQuantizedQpc = quantizedQpc;
132+
}
133+
134+
// Grab off the fractional portion of the preciseScaledResult. As an example:
135+
// QueryPerformanceFrequency is 1,000,000
136+
// This means a QPC is 1us.
137+
// Quantum of 1000us means a QPC of 125 would result in a fractional portion of 0.125
138+
// (math works out to (125 % 1000) / 1000).
139+
// This fractional portion is then evaluated in order to determine whether or not to snap
140+
// forward and use the next quantum, or use the floored one. This calculation gives us random
141+
// windows in which specific 1000us timestamps are observed for a non-deterministic amount of time.
142+
double preciseScaledFractional = preciseScaledResult - ((ULONGLONG)preciseScaledResult);
143+
if (preciseScaledFractional < currentRandomWindowScaled)
144+
{
145+
quantizedQpc = quantizedQpcFloored;
146+
}
147+
148+
return quantizedQpc;
149+
}
150+
};
151+
152+
// Set to jitter to an accuracy of 1000 ticks/second or 1ms
153+
static thread_local JitterManager<1000> timeJitter;
154+
155+
double GetQuantizedSystemTime()
156+
{
157+
SYSTEMTIME stTime;
158+
::GetSystemTime(&stTime);
159+
// In the event that we have no high-res timers available, we don't have the needed
160+
// granularity to jitter at a 1ms precision. We need to do something (as otherwise,
161+
// the timer precision is higher than on a high-precision machine), but the best we
162+
// can do is likely to strongly reduce the timer accuracy. Here we group by 4ms.
163+
stTime.wMilliseconds = (stTime.wMilliseconds / 4) * 4;
164+
return Js::DateUtilities::TimeFromSt(&stTime);
165+
}
14166

15167
double HiResTimer::GetSystemTime()
16168
{
@@ -38,15 +190,15 @@ namespace DateTime
38190
{
39191
if(!data.fHiResAvailable)
40192
{
41-
return GetSystemTime();
193+
return GetQuantizedSystemTime();
42194
}
43195

44196
if(!data.fInit)
45197
{
46198
if (!QueryPerformanceFrequency((LARGE_INTEGER *) &(data.freq)))
47199
{
48200
data.fHiResAvailable = false;
49-
return GetSystemTime();
201+
return GetQuantizedSystemTime();
50202
}
51203
data.fInit = true;
52204
}
@@ -60,11 +212,14 @@ namespace DateTime
60212
if( !QueryPerformanceCounter((LARGE_INTEGER *) &count))
61213
{
62214
data.fHiResAvailable = false;
63-
return GetSystemTime();
215+
return GetQuantizedSystemTime();
64216
}
65217

66218
double time = GetSystemTime();
67219

220+
// quantize the timestamp to hinder timing attacks
221+
count = timeJitter.QuantizedQPC(count);
222+
68223
// there is a base time and count set.
69224
if (!data.fReset
70225
&& (count >= data.baseMsCount)) // Make sure we don't regress

0 commit comments

Comments
 (0)