1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
|
/*
* Copyright (C) 2016 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ShadowChicken.h"
#include "CodeBlock.h"
#include "JSCInlines.h"
#include "ShadowChickenInlines.h"
#include <wtf/ListDump.h>
namespace JSC {
static const bool verbose = false;
void ShadowChicken::Packet::dump(PrintStream& out) const
{
if (!*this) {
out.print("empty");
return;
}
if (isPrologue()) {
out.print(
"{callee = ", RawPointer(callee), ", frame = ", RawPointer(frame), ", callerFrame = ",
RawPointer(callerFrame), "}");
return;
}
if (isTail()) {
out.print("tail-packet:{frame = ", RawPointer(frame), "}");
return;
}
ASSERT(isThrow());
out.print("throw");
}
void ShadowChicken::Frame::dump(PrintStream& out) const
{
out.print(
"{callee = ", RawPointer(callee), ", frame = ", RawPointer(frame), ", isTailDeleted = ",
isTailDeleted, "}");
}
ShadowChicken::ShadowChicken()
: m_logSize(Options::shadowChickenLogSize())
{
m_log = static_cast<Packet*>(fastZeroedMalloc(sizeof(Packet) * m_logSize));
m_logCursor = m_log;
m_logEnd = m_log + m_logSize;
}
ShadowChicken::~ShadowChicken()
{
fastFree(m_log);
}
void ShadowChicken::log(VM& vm, ExecState* exec, const Packet& packet)
{
update(vm, exec);
*m_logCursor++ = packet;
}
void ShadowChicken::update(VM& vm, ExecState* exec)
{
if (verbose) {
dataLog("Running update on: ", *this, "\n");
WTFReportBacktrace();
}
const unsigned logCursorIndex = m_logCursor - m_log;
// We need to figure out how to reconcile the current machine stack with our shadow stack. We do
// that by figuring out how much of the shadow stack to pop. We apply three different rules. The
// precise rule relies on the log. The log contains caller frames, which means that we know
// where we bottomed out after making any call. If we bottomed out but made no calls then 'exec'
// will tell us. That's why "highestPointSinceLastTime" will go no lower than exec. The third
// rule, based on comparing to the current real stack, is executed in a later loop.
CallFrame* highestPointSinceLastTime = exec;
for (unsigned i = logCursorIndex; i--;) {
Packet packet = m_log[i];
if (packet.isPrologue()) {
CallFrame* watermark;
if (i && m_log[i - 1].isTail())
watermark = packet.frame;
else
watermark = packet.callerFrame;
highestPointSinceLastTime = std::max(highestPointSinceLastTime, watermark);
}
}
if (verbose)
dataLog("Highest point since last time: ", RawPointer(highestPointSinceLastTime), "\n");
while (!m_stack.isEmpty() && (m_stack.last().frame < highestPointSinceLastTime || m_stack.last().isTailDeleted))
m_stack.removeLast();
if (verbose)
dataLog(" Revised stack: ", listDump(m_stack), "\n");
// It's possible that the top of stack is now tail-deleted. The stack no longer contains any
// frames below the log's high watermark. That means that we just need to look for the first
// occurence of a tail packet for the current stack top.
if (!m_stack.isEmpty()) {
ASSERT(!m_stack.last().isTailDeleted);
for (unsigned i = 0; i < logCursorIndex; ++i) {
Packet& packet = m_log[i];
if (packet.isTail() && packet.frame == m_stack.last().frame) {
Frame& frame = m_stack.last();
frame.thisValue = packet.thisValue;
frame.scope = packet.scope;
frame.codeBlock = packet.codeBlock;
frame.callSiteIndex = packet.callSiteIndex;
frame.isTailDeleted = true;
break;
}
}
}
if (verbose)
dataLog(" Revised stack: ", listDump(m_stack), "\n");
// The log-based and exec-based rules require that ShadowChicken was enabled. The point of
// ShadowChicken is to give sensible-looking results even if we had not logged. This means that
// we need to reconcile the shadow stack and the real stack by actually looking at the real
// stack. This reconciliation allows the shadow stack to have extra tail-deleted frames, but it
// forbids it from diverging from the real stack on normal frames.
if (!m_stack.isEmpty()) {
Vector<Frame> stackRightNow;
StackVisitor::visit(
exec, [&] (StackVisitor& visitor) -> StackVisitor::Status {
if (visitor->isInlinedFrame())
return StackVisitor::Continue;
if (visitor->isWasmFrame()) {
// FIXME: Make shadow chicken work with Wasm.
// https://bugs.webkit.org/show_bug.cgi?id=165441
return StackVisitor::Continue;
}
bool isTailDeleted = false;
// FIXME: Make shadow chicken work with Wasm.
// https://bugs.webkit.org/show_bug.cgi?id=165441
stackRightNow.append(Frame(jsCast<JSObject*>(visitor->callee()), visitor->callFrame(), isTailDeleted));
return StackVisitor::Continue;
});
stackRightNow.reverse();
if (verbose)
dataLog(" Stack right now: ", listDump(stackRightNow), "\n");
unsigned shadowIndex = 0;
unsigned rightNowIndex = 0;
while (shadowIndex < m_stack.size() && rightNowIndex < stackRightNow.size()) {
if (m_stack[shadowIndex].isTailDeleted) {
shadowIndex++;
continue;
}
// We specifically don't use operator== here because we are using a less
// strict filter on equality of frames. For example, the scope pointer
// could change, but we wouldn't want to consider the frames different entities
// because of that because it's natural for the program to change scopes.
if (m_stack[shadowIndex].frame == stackRightNow[rightNowIndex].frame
&& m_stack[shadowIndex].callee == stackRightNow[rightNowIndex].callee) {
shadowIndex++;
rightNowIndex++;
continue;
}
break;
}
m_stack.resize(shadowIndex);
if (verbose)
dataLog(" Revised stack: ", listDump(m_stack), "\n");
}
// It's possible that the top stack frame is actually lower than highestPointSinceLastTime.
// Account for that here.
highestPointSinceLastTime = nullptr;
for (unsigned i = m_stack.size(); i--;) {
if (!m_stack[i].isTailDeleted) {
highestPointSinceLastTime = m_stack[i].frame;
break;
}
}
if (verbose)
dataLog(" Highest point since last time: ", RawPointer(highestPointSinceLastTime), "\n");
// Set everything up so that we know where the top frame is in the log.
unsigned indexInLog = logCursorIndex;
auto advanceIndexInLogTo = [&] (CallFrame* frame, JSObject* callee, CallFrame* callerFrame) -> bool {
if (verbose)
dataLog(" Advancing to frame = ", RawPointer(frame), " from indexInLog = ", indexInLog, "\n");
if (indexInLog > logCursorIndex) {
if (verbose)
dataLog(" Bailing.\n");
return false;
}
unsigned oldIndexInLog = indexInLog;
while (indexInLog--) {
Packet packet = m_log[indexInLog];
// If all callees opt into ShadowChicken, then this search will rapidly terminate when
// we find our frame. But if our frame's callee didn't emit a prologue packet because it
// didn't opt in, then we will keep looking backwards until we *might* find a different
// frame. If we've been given the callee and callerFrame as a filter, then it's unlikely
// that we will hit the wrong frame. But we don't always have that information.
//
// This means it's worth adding other filters. For example, we could track changes in
// stack size. Once we've seen a frame at some height, we're no longer interested in
// frames below that height. Also, we can break as soon as we see a frame higher than
// the one we're looking for.
// FIXME: Add more filters.
// https://bugs.webkit.org/show_bug.cgi?id=155685
if (packet.isPrologue() && packet.frame == frame
&& (!callee || packet.callee == callee)
&& (!callerFrame || packet.callerFrame == callerFrame)) {
if (verbose)
dataLog(" Found at indexInLog = ", indexInLog, "\n");
return true;
}
}
// This is an interesting eventuality. We will see this if ShadowChicken was not
// consistently enabled. We have a choice between:
//
// - Leaving the log index at -1, which will prevent the log from being considered. This is
// the most conservative. It means that we will not be able to recover tail-deleted frames
// from anything that sits above a frame that didn't log a prologue packet. This means
// that everyone who creates prologues must log prologue packets.
//
// - Restoring the log index to what it was before. This prevents us from considering
// whether this frame has tail-deleted frames behind it, but that's about it. The problem
// with this approach is that it might recover tail-deleted frames that aren't relevant.
// I haven't thought about this too deeply, though.
//
// It seems like the latter option is less harmful, so that's what we do.
indexInLog = oldIndexInLog;
if (verbose)
dataLog(" Didn't find it.\n");
return false;
};
Vector<Frame> toPush;
StackVisitor::visit(
exec, [&] (StackVisitor& visitor) -> StackVisitor::Status {
if (visitor->isInlinedFrame()) {
// FIXME: Handle inlining.
// https://bugs.webkit.org/show_bug.cgi?id=155686
return StackVisitor::Continue;
}
if (visitor->isWasmFrame()) {
// FIXME: Make shadow chicken work with Wasm.
return StackVisitor::Continue;
}
CallFrame* callFrame = visitor->callFrame();
if (verbose)
dataLog(" Examining ", RawPointer(callFrame), "\n");
if (callFrame == highestPointSinceLastTime) {
if (verbose)
dataLog(" Bailing at ", RawPointer(callFrame), " because it's the highest point since last time.\n");
return StackVisitor::Done;
}
bool foundFrame = advanceIndexInLogTo(callFrame, callFrame->jsCallee(), callFrame->callerFrame());
bool isTailDeleted = false;
JSScope* scope = nullptr;
CodeBlock* codeBlock = callFrame->codeBlock();
if (codeBlock && codeBlock->wasCompiledWithDebuggingOpcodes() && codeBlock->scopeRegister().isValid()) {
scope = callFrame->scope(codeBlock->scopeRegister().offset());
RELEASE_ASSERT(scope->inherits(vm, JSScope::info()));
} else if (foundFrame) {
scope = m_log[indexInLog].scope;
if (scope)
RELEASE_ASSERT(scope->inherits(vm, JSScope::info()));
}
toPush.append(Frame(jsCast<JSObject*>(visitor->callee()), callFrame, isTailDeleted, callFrame->thisValue(), scope, codeBlock, callFrame->callSiteIndex()));
if (indexInLog < logCursorIndex
// This condition protects us from the case where advanceIndexInLogTo didn't find
// anything.
&& m_log[indexInLog].frame == toPush.last().frame) {
if (verbose)
dataLog(" Going to loop through to find tail deleted frames with indexInLog = ", indexInLog, " and push-stack top = ", toPush.last(), "\n");
for (;;) {
ASSERT(m_log[indexInLog].frame == toPush.last().frame);
// Right now the index is pointing at a prologue packet of the last frame that
// we pushed. Peek behind that packet to see if there is a tail packet. If there
// is one then we know that there is a corresponding prologue packet that will
// tell us about a tail-deleted frame.
if (!indexInLog)
break;
Packet tailPacket = m_log[indexInLog - 1];
if (!tailPacket.isTail()) {
// Last frame that we recorded was not the outcome of a tail call. So, there
// will not be any more deleted frames.
// FIXME: We might want to have a filter here. Consider that this was a tail
// marker for a tail call to something that didn't log anything. It should
// be sufficient to give the tail marker a copy of the caller frame.
// https://bugs.webkit.org/show_bug.cgi?id=155687
break;
}
indexInLog--; // Skip over the tail packet.
if (!advanceIndexInLogTo(tailPacket.frame, nullptr, nullptr)) {
if (verbose)
dataLog("Can't find prologue packet for tail: ", RawPointer(tailPacket.frame), "\n");
// We were unable to locate the prologue packet for this tail packet.
// This is rare but can happen in a situation like:
// function foo() {
// ... call some deeply tail-recursive function, causing a random number of log processings.
// return bar(); // tail call
// }
break;
}
Packet packet = m_log[indexInLog];
bool isTailDeleted = true;
RELEASE_ASSERT(tailPacket.scope->inherits(vm, JSScope::info()));
toPush.append(Frame(packet.callee, packet.frame, isTailDeleted, tailPacket.thisValue, tailPacket.scope, tailPacket.codeBlock, tailPacket.callSiteIndex));
}
}
return StackVisitor::Continue;
});
if (verbose)
dataLog(" Pushing: ", listDump(toPush), "\n");
for (unsigned i = toPush.size(); i--;)
m_stack.append(toPush[i]);
// We want to reset the log. There is a fun corner-case: there could be a tail marker at the end
// of this log. We could make that work by setting isTailDeleted on the top of stack, but that
// would require more corner cases in the complicated reconciliation code above. That code
// already knows how to handle a tail packet at the beginning, so we just leverage that here.
if (logCursorIndex && m_log[logCursorIndex - 1].isTail()) {
m_log[0] = m_log[logCursorIndex - 1];
m_logCursor = m_log + 1;
} else
m_logCursor = m_log;
if (verbose)
dataLog(" After pushing: ", *this, "\n");
// Remove tail frames until the number of tail deleted frames is small enough.
const unsigned maxTailDeletedFrames = Options::shadowChickenMaxTailDeletedFramesSize();
if (m_stack.size() > maxTailDeletedFrames) {
unsigned numberOfTailDeletedFrames = 0;
for (const Frame& frame : m_stack) {
if (frame.isTailDeleted)
numberOfTailDeletedFrames++;
}
if (numberOfTailDeletedFrames > maxTailDeletedFrames) {
unsigned dstIndex = 0;
unsigned srcIndex = 0;
while (srcIndex < m_stack.size()) {
Frame frame = m_stack[srcIndex++];
if (numberOfTailDeletedFrames > maxTailDeletedFrames && frame.isTailDeleted) {
numberOfTailDeletedFrames--;
continue;
}
m_stack[dstIndex++] = frame;
}
m_stack.resize(dstIndex);
}
}
if (verbose)
dataLog(" After clean-up: ", *this, "\n");
}
void ShadowChicken::visitChildren(SlotVisitor& visitor)
{
for (unsigned i = m_logCursor - m_log; i--;) {
JSObject* callee = m_log[i].callee;
if (callee != Packet::tailMarker() && callee != Packet::throwMarker())
visitor.appendUnbarriered(callee);
if (callee != Packet::throwMarker())
visitor.appendUnbarriered(m_log[i].scope);
if (callee == Packet::tailMarker()) {
visitor.appendUnbarriered(m_log[i].thisValue);
visitor.appendUnbarriered(m_log[i].codeBlock);
}
}
for (unsigned i = m_stack.size(); i--; ) {
Frame& frame = m_stack[i];
visitor.appendUnbarriered(frame.thisValue);
visitor.appendUnbarriered(frame.callee);
if (frame.scope)
visitor.appendUnbarriered(frame.scope);
if (frame.codeBlock)
visitor.appendUnbarriered(frame.codeBlock);
}
}
void ShadowChicken::reset()
{
m_logCursor = m_log;
m_stack.clear();
}
void ShadowChicken::dump(PrintStream& out) const
{
out.print("{stack = [", listDump(m_stack), "], log = [");
CommaPrinter comma;
unsigned limit = static_cast<unsigned>(m_logCursor - m_log);
out.print("\n");
for (unsigned i = 0; i < limit; ++i)
out.print("\t", comma, m_log[i], "\n");
out.print("]}");
}
JSArray* ShadowChicken::functionsOnStack(ExecState* exec)
{
VM& vm = exec->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
JSArray* result = constructEmptyArray(exec, 0);
RETURN_IF_EXCEPTION(scope, nullptr);
iterate(
vm, exec,
[&] (const Frame& frame) -> bool {
result->push(exec, frame.callee);
RELEASE_ASSERT(!scope.exception()); // This function is only called from tests.
return true;
});
return result;
}
} // namespace JSC
|