Bug 1280818 part 1 - Add the ability to capture the stack until the first non-self-hosted frame with the given principals; r=bz,jimb

Before this commit, one could either capture all stack frames (by passing
maxFrameCount = 0) or a maximum of N frames (by passing maxFrameCount = N). This
commit introduces the ability to capture the first frame (by default ignoring
self hosted frames) with some target principals. This new option required
replacing the `unsigned maxFrameCount` parameter with the introduction of a new
sum type to describe the stack capturing behavior:

  StackCapture = AllFrames
               | MaxFrames(unsigned n)
               | FirstSubsumedFrame(JSPrincipals* p, bool ignoreSelfHosted)

This is obviously more wordy in C++ than we'd like, but does make the stack
capturing more explicit rather than relying on the sentinal 0 to stand in for
infinity.
This commit is contained in:
Nick Fitzgerald 2016-07-21 23:40:59 -04:00
Родитель d4b9fb7cfb
Коммит ea16cfab73
9 изменённых файлов: 296 добавлений и 30 удалений

Просмотреть файл

@ -673,7 +673,10 @@ CreateStack(JSContext* aCx, int32_t aMaxDepth)
}
JS::Rooted<JSObject*> stack(aCx);
if (!JS::CaptureCurrentStack(aCx, &stack, aMaxDepth)) {
JS::StackCapture capture = aMaxDepth == 0
? JS::StackCapture(JS::AllFrames())
: JS::StackCapture(JS::MaxFrames(aMaxDepth));
if (!JS::CaptureCurrentStack(aCx, &stack, mozilla::Move(capture))) {
return nullptr;
}

Просмотреть файл

@ -135,7 +135,7 @@ PromiseObject::create(JSContext* cx, HandleObject executor, HandleObject proto /
// but oh well.
RootedObject stack(cx);
if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) {
if (!JS::CaptureCurrentStack(cx, &stack, 0))
if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames())))
return nullptr;
}
promise->setFixedSlot(PROMISE_ALLOCATION_SITE_SLOT, ObjectOrNullValue(stack));
@ -425,7 +425,7 @@ PromiseObject::onSettled(JSContext* cx)
Rooted<PromiseObject*> promise(cx, this);
RootedObject stack(cx);
if (cx->options().asyncStack() || cx->compartment()->isDebuggee()) {
if (!JS::CaptureCurrentStack(cx, &stack, 0)) {
if (!JS::CaptureCurrentStack(cx, &stack, JS::StackCapture(JS::AllFrames()))) {
cx->clearPendingException();
return;
}

Просмотреть файл

@ -1103,7 +1103,7 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
unsigned maxFrameCount = 0;
JS::StackCapture capture((JS::AllFrames()));
if (args.length() >= 1) {
double d;
if (!ToNumber(cx, args[0], &d))
@ -1114,7 +1114,8 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp)
"not a valid maximum frame count", NULL);
return false;
}
maxFrameCount = d;
if (d > 0)
capture = JS::StackCapture(JS::MaxFrames(d));
}
JSCompartment* targetCompartment = cx->compartment();
@ -1134,7 +1135,7 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp)
RootedObject stack(cx);
{
AutoCompartment ac(cx, targetCompartment);
if (!JS::CaptureCurrentStack(cx, &stack, maxFrameCount))
if (!JS::CaptureCurrentStack(cx, &stack, mozilla::Move(capture)))
return false;
}
@ -1145,6 +1146,37 @@ SaveStack(JSContext* cx, unsigned argc, Value* vp)
return true;
}
static bool
CaptureFirstSubsumedFrame(JSContext* cx, unsigned argc, JS::Value* vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
if (!args.requireAtLeast(cx, "captureFirstSubsumedFrame", 1))
return false;
if (!args[0].isObject()) {
JS_ReportError(cx, "The argument must be an object");
return false;
}
RootedObject obj(cx, &args[0].toObject());
obj = CheckedUnwrap(obj);
if (!obj) {
JS_ReportError(cx, "Denied permission to object.");
return false;
}
JS::StackCapture capture(JS::FirstSubsumedFrame(cx, obj->compartment()->principals()));
if (args.length() > 1)
capture.as<JS::FirstSubsumedFrame>().ignoreSelfHosted = JS::ToBoolean(args[1]);
JS::RootedObject capturedStack(cx);
if (!JS::CaptureCurrentStack(cx, &capturedStack, mozilla::Move(capture)))
return false;
args.rval().setObjectOrNull(capturedStack);
return true;
}
static bool
CallFunctionFromNativeFrame(JSContext* cx, unsigned argc, Value* vp)
{
@ -3603,6 +3635,12 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = {
" of frames. If 'compartment' is given, allocate the js::SavedFrame instances\n"
" with the given object's compartment."),
JS_FN_HELP("captureFirstSubsumedFrame", CaptureFirstSubsumedFrame, 1, 0,
"saveStack(object [, shouldIgnoreSelfHosted = true]])",
" Capture a stack back to the first frame whose principals are subsumed by the\n"
" object's compartment's principals. If 'shouldIgnoreSelfHosted' is given,\n"
" control whether self-hosted frames are considered when checking principals."),
JS_FN_HELP("callFunctionFromNativeFrame", CallFunctionFromNativeFrame, 1, 0,
"callFunctionFromNativeFrame(function)",
" Call 'function' with a (C++-)native frame on stack.\n"

Просмотреть файл

@ -0,0 +1,92 @@
// Create two different globals whose compartments have two different
// principals. Test getting the first frame on the stack with some given
// principals in various configurations of JS stack and of wanting self-hosted
// frames or not.
const g1 = newGlobal({
principal: 0xffff
});
const g2 = newGlobal({
principal: 0xff
});
// Introduce everyone to themselves and each other.
g1.g2 = g2.g2 = g2;
g1.g1 = g2.g1 = g1;
g1.g2obj = g2.eval("new Object");
g1.evaluate(`
const global = this;
// Capture the stack back to the first frame in the g2 global.
function capture(shouldIgnoreSelfHosted = true) {
return captureFirstSubsumedFrame(global.g2obj, shouldIgnoreSelfHosted);
}
`, {
fileName: "script1.js"
});
g2.evaluate(`
const capture = g1.capture;
// Use our Function.prototype.bind, not capture.bind (which is ===
// g1.Function.prototype.bind) so that the generated bound function is in our
// compartment and has our principals.
const boundTrue = Function.prototype.bind.call(capture, null, true);
const boundFalse = Function.prototype.bind.call(capture, null, false);
function getOldestFrame(stack) {
while (stack.parent) {
stack = stack.parent;
}
return stack;
}
function dumpStack(name, stack) {
print("Stack " + name + " =");
while (stack) {
print(" " + stack.functionDisplayName + " @ " + stack.source);
stack = stack.parent;
}
print();
}
// When the youngest frame is not self-hosted, it doesn't matter whether or not
// we specify that we should ignore self hosted frames when capturing the first
// frame with the given principals.
//
// Stack: iife1 (g2) <- capture (g1)
(function iife1() {
const captureTrueStack = capture(true);
dumpStack("captureTrueStack", captureTrueStack);
assertEq(getOldestFrame(captureTrueStack).functionDisplayName, "iife1");
assertEq(getOldestFrame(captureTrueStack).source, "script2.js");
const captureFalseStack = capture(false);
dumpStack("captureFalseStack", captureFalseStack);
assertEq(getOldestFrame(captureFalseStack).functionDisplayName, "iife1");
assertEq(getOldestFrame(captureFalseStack).source, "script2.js");
}());
// When the youngest frame is a self hosted frame, we get two different
// captured stacks depending on whether or not we ignore self-hosted frames.
//
// Stack: iife2 (g2) <- bound function (g2) <- capture (g1)
(function iife2() {
const boundTrueStack = boundTrue();
dumpStack("boundTrueStack", boundTrueStack);
assertEq(getOldestFrame(boundTrueStack).functionDisplayName, "iife2");
assertEq(getOldestFrame(boundTrueStack).source, "script2.js");
const boundFalseStack = boundFalse();
dumpStack("boundFalseStack", boundFalseStack);
assertEq(getOldestFrame(boundFalseStack).functionDisplayName !== "iife2", true);
assertEq(getOldestFrame(boundFalseStack).source, "self-hosted");
}());
`, {
fileName: "script2.js"
});

Просмотреть файл

@ -6536,8 +6536,14 @@ JS::SetOutOfMemoryCallback(JSContext* cx, OutOfMemoryCallback cb, void* data)
cx->oomCallbackData = data;
}
JS::FirstSubsumedFrame::FirstSubsumedFrame(JSContext* cx,
bool ignoreSelfHostedFrames /* = true */)
: JS::FirstSubsumedFrame(cx, cx->compartment()->principals(), ignoreSelfHostedFrames)
{ }
JS_PUBLIC_API(bool)
JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp, unsigned maxFrameCount)
JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp,
JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */)
{
AssertHeapIsIdle(cx);
CHECK_REQUEST(cx);
@ -6545,7 +6551,7 @@ JS::CaptureCurrentStack(JSContext* cx, JS::MutableHandleObject stackp, unsigned
JSCompartment* compartment = cx->compartment();
Rooted<SavedFrame*> frame(cx);
if (!compartment->savedStacks().saveCurrentStack(cx, &frame, maxFrameCount))
if (!compartment->savedStacks().saveCurrentStack(cx, &frame, mozilla::Move(capture)))
return false;
stackp.set(frame.get());
return true;

Просмотреть файл

@ -5896,14 +5896,96 @@ typedef void
extern JS_PUBLIC_API(void)
SetOutOfMemoryCallback(JSContext* cx, OutOfMemoryCallback cb, void* data);
/**
* Capture all frames.
*/
struct AllFrames { };
/**
* Capture at most this many frames.
*/
struct MaxFrames
{
unsigned maxFrames;
explicit MaxFrames(unsigned max)
: maxFrames(max)
{
MOZ_ASSERT(max > 0);
}
};
/**
* Capture the first frame with the given principals. By default, do not
* consider self-hosted frames with the given principals as satisfying the stack
* capture.
*/
struct FirstSubsumedFrame
{
JSContext* cx;
JSPrincipals* principals;
bool ignoreSelfHosted;
/**
* Use the cx's current compartment's principals.
*/
explicit FirstSubsumedFrame(JSContext* cx, bool ignoreSelfHostedFrames = true);
explicit FirstSubsumedFrame(JSContext* ctx, JSPrincipals* p, bool ignoreSelfHostedFrames = true)
: cx(ctx)
, principals(p)
, ignoreSelfHosted(ignoreSelfHostedFrames)
{
JS_HoldPrincipals(principals);
}
// No copying because we want to avoid holding and dropping principals
// unnecessarily.
FirstSubsumedFrame(const FirstSubsumedFrame&) = delete;
FirstSubsumedFrame& operator=(const FirstSubsumedFrame&) = delete;
FirstSubsumedFrame(FirstSubsumedFrame&& rhs)
: principals(rhs.principals)
, ignoreSelfHosted(rhs.ignoreSelfHosted)
{
MOZ_ASSERT(this != &rhs, "self move disallowed");
rhs.principals = nullptr;
}
FirstSubsumedFrame& operator=(FirstSubsumedFrame&& rhs) {
new (this) FirstSubsumedFrame(mozilla::Move(rhs));
return *this;
}
~FirstSubsumedFrame() {
if (principals)
JS_DropPrincipals(cx, principals);
}
};
using StackCapture = mozilla::Variant<AllFrames, MaxFrames, FirstSubsumedFrame>;
/**
* Capture the current call stack as a chain of SavedFrame JSObjects, and set
* |stackp| to the SavedFrame for the youngest stack frame, or nullptr if there
* are no JS frames on the stack. If |maxFrameCount| is non-zero, capture at
* most the youngest |maxFrameCount| frames.
* are no JS frames on the stack.
*
* The |capture| parameter describes the portion of the JS stack to capture:
*
* * |JS::AllFrames|: Capture all frames on the stack.
*
* * |JS::MaxFrames|: Capture no more than |JS::MaxFrames::maxFrames| from the
* stack.
*
* * |JS::FirstSubsumedFrame|: Capture the first frame whose principals are
* subsumed by |JS::FirstSubsumedFrame::principals|. By default, do not
* consider self-hosted frames; this can be controlled via the
* |JS::FirstSubsumedFrame::ignoreSelfHosted| flag. Do not capture any async
* stack.
*/
extern JS_PUBLIC_API(bool)
CaptureCurrentStack(JSContext* cx, MutableHandleObject stackp, unsigned maxFrameCount = 0);
CaptureCurrentStack(JSContext* cx, MutableHandleObject stackp,
StackCapture&& capture = StackCapture(AllFrames()));
/*
* This is a utility function for preparing an async stack to be used

Просмотреть файл

@ -266,7 +266,8 @@ static const size_t MAX_REPORTED_STACK_DEPTH = 1u << 7;
static bool
CaptureStack(JSContext* cx, MutableHandleObject stack)
{
return CaptureCurrentStack(cx, stack, MAX_REPORTED_STACK_DEPTH);
return CaptureCurrentStack(cx, stack,
JS::StackCapture(JS::MaxFrames(MAX_REPORTED_STACK_DEPTH)));
}
JSString*

Просмотреть файл

@ -1068,7 +1068,8 @@ SavedStacks::init()
}
bool
SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, unsigned maxFrameCount)
SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame,
JS::StackCapture&& capture /* = JS::StackCapture(JS::AllFrames()) */)
{
MOZ_ASSERT(initialized());
MOZ_RELEASE_ASSERT(cx->compartment());
@ -1085,7 +1086,7 @@ SavedStacks::saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame, unsi
AutoSPSEntry psuedoFrame(cx->runtime(), "js::SavedStacks::saveCurrentStack");
FrameIter iter(cx);
return insertFrames(cx, iter, frame, maxFrameCount);
return insertFrames(cx, iter, frame, mozilla::Move(capture));
}
bool
@ -1137,9 +1138,49 @@ SavedStacks::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf)
pcLocationMap.sizeOfExcludingThis(mallocSizeOf);
}
// Given that we have captured a stqck frame with the given principals and
// source, return true if the requested `StackCapture` has been satisfied and
// stack walking can halt. Return false otherwise (and stack walking and frame
// capturing should continue).
static inline bool
captureIsSatisfied(JSContext* cx, JSPrincipals* principals, const JSAtom* source,
JS::StackCapture& capture)
{
class Matcher
{
JSContext* cx_;
JSPrincipals* framePrincipals_;
const JSAtom* frameSource_;
public:
Matcher(JSContext* cx, JSPrincipals* principals, const JSAtom* source)
: cx_(cx)
, framePrincipals_(principals)
, frameSource_(source)
{ }
bool match(JS::FirstSubsumedFrame& target) {
auto subsumes = cx_->runtime()->securityCallbacks->subsumes;
return (!subsumes || subsumes(target.principals, framePrincipals_)) &&
(!target.ignoreSelfHosted || frameSource_ != cx_->names().selfHosted);
}
bool match(JS::MaxFrames& target) {
return target.maxFrames == 1;
}
bool match(JS::AllFrames&) {
return false;
}
};
Matcher m(cx, principals, source);
return capture.match(m);
}
bool
SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFrame frame,
unsigned maxFrameCount)
JS::StackCapture&& capture)
{
// In order to lookup a cached SavedFrame object, we need to have its parent
// SavedFrame, which means we need to walk the stack from oldest frame to
@ -1216,9 +1257,10 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram
// The bit set means that the next older parent (frame, pc) pair *must*
// be in the cache.
if (maxFrameCount == 0)
if (capture.is<JS::AllFrames>())
parentIsInCache = iter.hasCachedSavedFrame();
auto principals = iter.compartment()->principals();
auto displayAtom = iter.isFunctionFrame() ? iter.functionDisplayAtom() : nullptr;
if (!stackChain->emplaceBack(location.source(),
location.line(),
@ -1226,7 +1268,7 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram
displayAtom,
nullptr,
nullptr,
iter.compartment()->principals(),
principals,
LiveSavedFrameCache::getFramePtr(iter),
iter.pc(),
&activation))
@ -1235,15 +1277,15 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram
return false;
}
++iter;
if (maxFrameCount == 1) {
if (captureIsSatisfied(cx, principals, location.source(), capture)) {
// The frame we just saved was the last one we were asked to save.
// If we had an async stack, ensure we don't use any of its frames.
asyncStack.set(nullptr);
break;
}
++iter;
if (parentIsInCache &&
!iter.done() &&
iter.hasCachedSavedFrame())
@ -1256,19 +1298,21 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram
break;
}
// If maxFrameCount is zero there's no limit on the number of frames.
if (maxFrameCount == 0)
continue;
maxFrameCount--;
if (capture.is<JS::MaxFrames>())
capture.as<JS::MaxFrames>().maxFrames--;
}
// Limit the depth of the async stack, if any, and ensure that the
// SavedFrame instances we use are stored in the same compartment as the
// rest of the synchronous stack chain.
RootedSavedFrame parentFrame(cx, cachedFrame);
if (asyncStack && !adoptAsyncStack(cx, asyncStack, asyncCause, &parentFrame, maxFrameCount))
return false;
if (asyncStack && !capture.is<JS::FirstSubsumedFrame>()) {
unsigned maxAsyncFrames = capture.is<JS::MaxFrames>()
? capture.as<JS::MaxFrames>().maxFrames
: ASYNC_STACK_MAX_FRAME_COUNT;
if (!adoptAsyncStack(cx, asyncStack, asyncCause, &parentFrame, maxAsyncFrames))
return false;
}
// Iterate through |stackChain| in reverse order and get or create the
// actual SavedFrame instances.
@ -1279,7 +1323,7 @@ SavedStacks::insertFrames(JSContext* cx, FrameIter& iter, MutableHandleSavedFram
if (!parentFrame)
return false;
if (maxFrameCount == 0 && lookup->framePtr && parentFrame != cachedFrame) {
if (capture.is<JS::AllFrames>() && lookup->framePtr && parentFrame != cachedFrame) {
auto* cache = lookup->activation->getLiveSavedFrameCache(cx);
if (!cache || !cache->insert(cx, *lookup->framePtr, lookup->pc, parentFrame))
return false;

Просмотреть файл

@ -165,7 +165,7 @@ class SavedStacks {
MOZ_MUST_USE bool init();
bool initialized() const { return frames.initialized(); }
MOZ_MUST_USE bool saveCurrentStack(JSContext* cx, MutableHandleSavedFrame frame,
unsigned maxFrameCount = 0);
JS::StackCapture&& capture = JS::StackCapture(JS::AllFrames()));
MOZ_MUST_USE bool copyAsyncStack(JSContext* cx, HandleObject asyncStack,
HandleString asyncCause,
MutableHandleSavedFrame adoptedStack,
@ -221,7 +221,7 @@ class SavedStacks {
MOZ_MUST_USE bool insertFrames(JSContext* cx, FrameIter& iter,
MutableHandleSavedFrame frame,
unsigned maxFrameCount = 0);
JS::StackCapture&& capture);
MOZ_MUST_USE bool adoptAsyncStack(JSContext* cx, HandleSavedFrame asyncStack,
HandleString asyncCause,
MutableHandleSavedFrame adoptedStack,