From 965878e88a9211b6995d57fddccf22ed365a9772 Mon Sep 17 00:00:00 2001 From: Lars Knoll Date: Mon, 11 Nov 2013 11:22:24 +0100 Subject: [PATCH] Limit the amount of memory we allocate on the stack Setup limits for both the C and the JS stack, and check them before entering functions. If we run out of space, throw a RangeError exception. Be careful and recheck the stack bounds when things go outside. This catches the case where the engine got moved to another thread changing the stack boundaries. Windows currently uses an unsafe fallback implementation, this needs to be fixed later on. Task-number: QTBUG-34568 Change-Id: I22fbcbec57b28f9cc8a49e12f1cc6e53e4f07888 Reviewed-by: Erik Verbruggen Reviewed-by: Gunnar Sletta --- src/qml/jsruntime/qv4context.cpp | 7 +++ src/qml/jsruntime/qv4context_p.h | 1 + src/qml/jsruntime/qv4engine.cpp | 65 +++++++++++++++++++++- src/qml/jsruntime/qv4engine_p.h | 14 ++++- src/qml/jsruntime/qv4functionobject.cpp | 7 +++ src/qml/jsruntime/qv4script.cpp | 2 + .../auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp | 6 ++ 7 files changed, 99 insertions(+), 3 deletions(-) diff --git a/src/qml/jsruntime/qv4context.cpp b/src/qml/jsruntime/qv4context.cpp index 18b0de3..97247ad 100644 --- a/src/qml/jsruntime/qv4context.cpp +++ b/src/qml/jsruntime/qv4context.cpp @@ -579,6 +579,13 @@ ReturnedValue ExecutionContext::throwRangeError(const ValueRef value) return throwError(error); } +ReturnedValue ExecutionContext::throwRangeError(const QString &message) +{ + Scope scope(this); + ScopedObject error(scope, engine->newRangeErrorObject(message)); + return throwError(error); +} + ReturnedValue ExecutionContext::throwURIError(const ValueRef msg) { Scope scope(this); diff --git a/src/qml/jsruntime/qv4context_p.h b/src/qml/jsruntime/qv4context_p.h index 9a7b9a6..ccb5cf9 100644 --- a/src/qml/jsruntime/qv4context_p.h +++ b/src/qml/jsruntime/qv4context_p.h @@ -136,6 +136,7 @@ struct Q_QML_EXPORT ExecutionContext ReturnedValue throwReferenceError(const ValueRef value); ReturnedValue throwReferenceError(const QString &value, const QString &fileName, int line, int column); ReturnedValue throwRangeError(const ValueRef value); + ReturnedValue throwRangeError(const QString &message); ReturnedValue throwURIError(const ValueRef msg); ReturnedValue throwUnimplemented(const QString &message); diff --git a/src/qml/jsruntime/qv4engine.cpp b/src/qml/jsruntime/qv4engine.cpp index 7af3133..7189e60 100644 --- a/src/qml/jsruntime/qv4engine.cpp +++ b/src/qml/jsruntime/qv4engine.cpp @@ -72,6 +72,10 @@ #include "qv4isel_moth_p.h" +#if USE(PTHREADS) +# include +#endif + QT_BEGIN_NAMESPACE using namespace QV4; @@ -83,6 +87,42 @@ static ReturnedValue throwTypeError(CallContext *ctx) return ctx->throwTypeError(); } +quintptr getStackLimit() +{ + quintptr stackLimit; +#if USE(PTHREADS) +# if OS(DARWIN) + pthread_t thread_self = pthread_self(); + void *stackTop = pthread_get_stackaddr_np(thread_self); + stackLimit = reinterpret_cast(stackTop); + stackLimit -= pthread_get_stacksize_np(thread_self); +# else + void* stackBottom = 0; + pthread_attr_t attr; + pthread_getattr_np(pthread_self(), &attr); + size_t stackSize = 0; + pthread_attr_getstack(&attr, &stackBottom, &stackSize); + pthread_attr_destroy(&attr); + + stackLimit = reinterpret_cast(stackBottom); +# endif +// This is wrong. StackLimit is the currently committed stack size, not the real end. +// only way to get that limit is apparently by using VirtualQuery (Yuck) +//#elif OS(WINDOWS) +// PNT_TIB tib = (PNT_TIB)NtCurrentTeb(); +// stackLimit = static_cast(tib->StackLimit); +#else + int dummy; + // this is inexact, as part of the stack is used when being called here, + // but let's simply default to 1MB from where the stack is right now + stackLimit = reinterpret_cast(&dummy) - 1024*1024; +#endif + + // 256k slack + return stackLimit + 256*1024; +} + + ExecutionEngine::ExecutionEngine(QQmlJS::EvalISelFactory *factory) : memoryManager(new QV4::MemoryManager) , executableAllocator(new QV4::ExecutableAllocator) @@ -118,11 +158,17 @@ ExecutionEngine::ExecutionEngine(QQmlJS::EvalISelFactory *factory) memoryManager->setExecutionEngine(this); - // reserve 8MB for the JS stack - *jsStack = WTF::PageAllocation::allocate(8*1024*1024, WTF::OSAllocator::JSVMStackPages, true); + // reserve space for the JS stack + // we allow it to grow to 2 times JSStackLimit, as we can overshoot due to garbage collection + // and ScopedValues allocated outside of JIT'ed methods. + *jsStack = WTF::PageAllocation::allocate(2*JSStackLimit, WTF::OSAllocator::JSVMStackPages, true); jsStackBase = (SafeValue *)jsStack->base(); jsStackTop = jsStackBase; + // set up stack limits + jsStackLimit = jsStackBase + JSStackLimit/sizeof(SafeValue); + cStackLimit = getStackLimit(); + Scope scope(this); identifierTable = new IdentifierTable(this); @@ -867,4 +913,19 @@ QQmlError ExecutionEngine::catchExceptionAsQmlError(ExecutionContext *context) return error; } +bool ExecutionEngine::recheckCStackLimits() +{ + int dummy; +#ifdef Q_OS_WIN + // ### this is only required on windows, where we currently use heuristics to get the stack limit + if (cStackLimit - reinterpret_cast(&dummy) > 128*1024) + // we're more then 128k away from our stack limit, assume the thread has changed, and + // call getStackLimit +#endif + // this can happen after a thread change + cStackLimit = getStackLimit(); + + return (reinterpret_cast(&dummy) >= cStackLimit); +} + QT_END_NAMESPACE diff --git a/src/qml/jsruntime/qv4engine_p.h b/src/qml/jsruntime/qv4engine_p.h index 2df148a..b497290 100644 --- a/src/qml/jsruntime/qv4engine_p.h +++ b/src/qml/jsruntime/qv4engine_p.h @@ -113,6 +113,12 @@ class RegExpCache; struct QmlExtensions; struct Exception; +#define CHECK_STACK_LIMITS(v4) \ + if ((v4->jsStackTop <= v4->jsStackLimit) && (reinterpret_cast(&v4) >= v4->cStackLimit || v4->recheckCStackLimits())) {} \ + else \ + return v4->current->throwRangeError(QStringLiteral("Maximum call stack size exceeded.")) + + struct Q_QML_EXPORT ExecutionEngine { MemoryManager *memoryManager; @@ -123,11 +129,15 @@ struct Q_QML_EXPORT ExecutionEngine ExecutionContext *current; GlobalContext *rootContext; + SafeValue *jsStackTop; + SafeValue *jsStackLimit; + quintptr cStackLimit; + WTF::BumpPointerAllocator *bumperPointerAllocator; // Used by Yarr Regex engine. + enum { JSStackLimit = 4*1024*1024 }; WTF::PageAllocation *jsStack; SafeValue *jsStackBase; - SafeValue *jsStackTop; SafeValue *stackPush(uint nValues) { SafeValue *ptr = jsStackTop; @@ -329,6 +339,8 @@ struct Q_QML_EXPORT ExecutionEngine QmlExtensions *qmlExtensions(); + bool recheckCStackLimits(); + // Exception handling SafeValue exceptionValue; quint32 hasException; diff --git a/src/qml/jsruntime/qv4functionobject.cpp b/src/qml/jsruntime/qv4functionobject.cpp index 7841f7c..aa1cb89 100644 --- a/src/qml/jsruntime/qv4functionobject.cpp +++ b/src/qml/jsruntime/qv4functionobject.cpp @@ -438,6 +438,7 @@ ReturnedValue ScriptFunction::construct(Managed *that, CallData *callData) ExecutionEngine *v4 = that->internalClass->engine; if (v4->hasException) return Encode::undefined(); + CHECK_STACK_LIMITS(v4); Scope scope(v4); Scoped f(scope, static_cast(that)); @@ -468,6 +469,7 @@ ReturnedValue ScriptFunction::call(Managed *that, CallData *callData) ExecutionEngine *v4 = f->internalClass->engine; if (v4->hasException) return Encode::undefined(); + CHECK_STACK_LIMITS(v4); ExecutionContext *context = v4->current; Scope scope(context); @@ -523,6 +525,7 @@ ReturnedValue SimpleScriptFunction::construct(Managed *that, CallData *callData) ExecutionEngine *v4 = that->internalClass->engine; if (v4->hasException) return Encode::undefined(); + CHECK_STACK_LIMITS(v4); Scope scope(v4); Scoped f(scope, static_cast(that)); @@ -566,6 +569,7 @@ ReturnedValue SimpleScriptFunction::call(Managed *that, CallData *callData) ExecutionEngine *v4 = that->internalClass->engine; if (v4->hasException) return Encode::undefined(); + CHECK_STACK_LIMITS(v4); SimpleScriptFunction *f = static_cast(that); @@ -617,6 +621,7 @@ ReturnedValue BuiltinFunction::call(Managed *that, CallData *callData) ExecutionEngine *v4 = f->internalClass->engine; if (v4->hasException) return Encode::undefined(); + CHECK_STACK_LIMITS(v4); ExecutionContext *context = v4->current; @@ -636,6 +641,8 @@ ReturnedValue IndexedBuiltinFunction::call(Managed *that, CallData *callData) ExecutionEngine *v4 = f->internalClass->engine; if (v4->hasException) return Encode::undefined(); + CHECK_STACK_LIMITS(v4); + ExecutionContext *context = v4->current; Scope scope(v4); diff --git a/src/qml/jsruntime/qv4script.cpp b/src/qml/jsruntime/qv4script.cpp index 26cb0d9..25791cf 100644 --- a/src/qml/jsruntime/qv4script.cpp +++ b/src/qml/jsruntime/qv4script.cpp @@ -89,6 +89,8 @@ QmlBindingWrapper::QmlBindingWrapper(ExecutionContext *scope, ObjectRef qml) ReturnedValue QmlBindingWrapper::call(Managed *that, CallData *) { ExecutionEngine *engine = that->engine(); + CHECK_STACK_LIMITS(engine); + Scope scope(engine); QmlBindingWrapper *This = static_cast(that); Q_ASSERT(This->function); diff --git a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp index 0f6d0b0..52791cd 100644 --- a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp +++ b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp @@ -313,6 +313,7 @@ private slots: void singletonFromQMLToCpp(); void setPropertyOnInvalid(); void miscTypeTest(); + void stackLimits(); private: // static void propertyVarWeakRefCallback(v8::Persistent object, void* parameter); @@ -7404,7 +7405,12 @@ void tst_qqmlecmascript::miscTypeTest() QVERIFY(q.toBool() == true); delete object; +} +void tst_qqmlecmascript::stackLimits() +{ + QJSEngine engine; + engine.evaluate(QStringLiteral("function foo() {foo();} try {foo()} catch(e) { }")); } QTEST_MAIN(tst_qqmlecmascript) -- 2.7.4