6
6
#include " RuntimePlatformAgnosticPch.h"
7
7
#include " Common.h"
8
8
#include " ChakraPlatform.h"
9
+ #include < Bcrypt.h>
9
10
10
11
namespace PlatformAgnostic
11
12
{
12
13
namespace DateTime
13
14
{
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
+ }
14
166
15
167
double HiResTimer::GetSystemTime ()
16
168
{
@@ -38,15 +190,15 @@ namespace DateTime
38
190
{
39
191
if (!data.fHiResAvailable )
40
192
{
41
- return GetSystemTime ();
193
+ return GetQuantizedSystemTime ();
42
194
}
43
195
44
196
if (!data.fInit )
45
197
{
46
198
if (!QueryPerformanceFrequency ((LARGE_INTEGER *) &(data.freq )))
47
199
{
48
200
data.fHiResAvailable = false ;
49
- return GetSystemTime ();
201
+ return GetQuantizedSystemTime ();
50
202
}
51
203
data.fInit = true ;
52
204
}
@@ -60,11 +212,14 @@ namespace DateTime
60
212
if ( !QueryPerformanceCounter ((LARGE_INTEGER *) &count))
61
213
{
62
214
data.fHiResAvailable = false ;
63
- return GetSystemTime ();
215
+ return GetQuantizedSystemTime ();
64
216
}
65
217
66
218
double time = GetSystemTime ();
67
219
220
+ // quantize the timestamp to hinder timing attacks
221
+ count = timeJitter.QuantizedQPC (count);
222
+
68
223
// there is a base time and count set.
69
224
if (!data.fReset
70
225
&& (count >= data.baseMsCount )) // Make sure we don't regress
0 commit comments