Limit the amount of memory we allocate on the stack
authorLars Knoll <lars.knoll@digia.com>
Mon, 11 Nov 2013 10:22:24 +0000 (11:22 +0100)
committerThe Qt Project <gerrit-noreply@qt-project.org>
Mon, 18 Nov 2013 10:05:25 +0000 (11:05 +0100)
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 <erik.verbruggen@digia.com>
Reviewed-by: Gunnar Sletta <gunnar.sletta@digia.com>
src/qml/jsruntime/qv4context.cpp
src/qml/jsruntime/qv4context_p.h
src/qml/jsruntime/qv4engine.cpp
src/qml/jsruntime/qv4engine_p.h
src/qml/jsruntime/qv4functionobject.cpp
src/qml/jsruntime/qv4script.cpp
tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp

index 18b0de3..97247ad 100644 (file)
@@ -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);
index 9a7b9a6..ccb5cf9 100644 (file)
@@ -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);
 
index 7af3133..7189e60 100644 (file)
 
 #include "qv4isel_moth_p.h"
 
+#if USE(PTHREADS)
+#  include <pthread.h>
+#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<quintptr>(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<quintptr>(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<quintptr>(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<qintptr>(&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<quintptr>(&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<quintptr>(&dummy) >= cStackLimit);
+}
+
 QT_END_NAMESPACE
index 2df148a..b497290 100644 (file)
@@ -113,6 +113,12 @@ class RegExpCache;
 struct QmlExtensions;
 struct Exception;
 
+#define CHECK_STACK_LIMITS(v4) \
+    if ((v4->jsStackTop <= v4->jsStackLimit) && (reinterpret_cast<quintptr>(&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;
index 7841f7c..aa1cb89 100644 (file)
@@ -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<ScriptFunction> f(scope, static_cast<ScriptFunction *>(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<SimpleScriptFunction> f(scope, static_cast<SimpleScriptFunction *>(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<SimpleScriptFunction *>(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);
 
index 26cb0d9..25791cf 100644 (file)
@@ -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<QmlBindingWrapper *>(that);
     Q_ASSERT(This->function);
index 0f6d0b0..52791cd 100644 (file)
@@ -313,6 +313,7 @@ private slots:
     void singletonFromQMLToCpp();
     void setPropertyOnInvalid();
     void miscTypeTest();
+    void stackLimits();
 
 private:
 //    static void propertyVarWeakRefCallback(v8::Persistent<v8::Value> 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)