From bb817d011a69a176752eb8637ff969a0ed579c11 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 13:35:06 -0500 Subject: [PATCH 1/6] QuickJS: bound the JS stack to the thread's real stack; Chakra: define globalThis QuickJS guards JS recursion against stack_top - JS_DEFAULT_STACK_SIZE, which is 1 MiB in quickjs-ng -- also the default size of a non-main thread on Android and Windows. On those threads the check sits below the guard page, so deep recursion faults before QuickJS can raise "InternalError: stack overflow": Android_QuickJS died with SIGSEGV (one frame repeated 150+ deep) in the Fetch polyfill's 40k-chunk Response test, while the 8 MiB-stacked desktop QuickJS jobs were fine. Derive the limit from the running thread (pthread_getattr_np / GetCurrentThreadStackLimits) minus a margin for native frames. The Windows 10 Chakra predates ES2020 and has no `globalThis`; every Fetch test failed there with "ReferenceError: 'globalThis' is not defined". Define it on the global object at env attach, as a plain writable configurable property. --- Core/AppRuntime/Source/AppRuntime_QuickJS.cpp | 55 +++++++++++++++++++ Core/Node-API/Source/env_chakra.cc | 13 +++++ 2 files changed, 68 insertions(+) diff --git a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp index 10505fea..d9ab764c 100644 --- a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp +++ b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp @@ -18,8 +18,62 @@ #pragma warning(pop) #endif +#if !defined(_WIN32) +#include +#endif +#if defined(_WIN32) +#include +#endif + +#include +#include +#include + namespace Babylon { + namespace + { + // QuickJS guards against JS recursion by comparing the C stack pointer against + // stack_top - stack_size, where stack_size defaults to JS_DEFAULT_STACK_SIZE (1 MiB in + // quickjs-ng). That is also the default size of a non-main thread on Android and Windows, + // so on those threads the limit sits below the real guard page: deep recursion faults + // (SIGSEGV) before QuickJS can raise "InternalError: stack overflow". Derive the limit + // from the thread that actually runs the runtime instead, keeping a margin for the native + // frames QuickJS and the host add between the check and the guard page. + size_t JavaScriptStackLimit() + { + constexpr size_t Margin{256 * 1024}; + constexpr size_t Fallback{512 * 1024}; + size_t threadStack{}; +#if defined(_WIN32) + ULONG_PTR low{}; + ULONG_PTR high{}; + GetCurrentThreadStackLimits(&low, &high); + threadStack = static_cast(high - low); +#elif defined(__APPLE__) + // pthread_getattr_np is a GNU/bionic extension; Apple exposes the size directly. + threadStack = pthread_get_stacksize_np(pthread_self()); +#else + pthread_attr_t attributes; + if (pthread_getattr_np(pthread_self(), &attributes) == 0) + { + void* base{}; + size_t size{}; + if (pthread_attr_getstack(&attributes, &base, &size) == 0) + { + threadStack = size; + } + pthread_attr_destroy(&attributes); + } +#endif + if (threadStack <= Margin) + { + return Fallback; + } + return std::min(threadStack - Margin, static_cast(JS_DEFAULT_STACK_SIZE) * 8); + } + } + void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/) { // Create the runtime. @@ -28,6 +82,7 @@ namespace Babylon { throw std::runtime_error{"Failed to create QuickJS runtime"}; } + JS_SetMaxStackSize(runtime, JavaScriptStackLimit()); // Create the context. JSContext* context = JS_NewContext(runtime); diff --git a/Core/Node-API/Source/env_chakra.cc b/Core/Node-API/Source/env_chakra.cc index 0c0be91c..641467df 100644 --- a/Core/Node-API/Source/env_chakra.cc +++ b/Core/Node-API/Source/env_chakra.cc @@ -23,6 +23,19 @@ namespace Napi JsValueRef global; ThrowIfFailed(JsGetGlobalObject(&global)); JsPropertyIdRef propertyId; + + // The Windows 10 Chakra predates ES2020 and has no `globalThis`; scripts written against + // browsers (and the polyfills in this repo) reference it. Define it as a plain, writable, + // configurable property of the global object, exactly as the spec describes. + ThrowIfFailed(JsGetPropertyIdFromName(L"globalThis", &propertyId)); + JsValueRef existingGlobalThis; + ThrowIfFailed(JsGetProperty(global, propertyId, &existingGlobalThis)); + JsValueType existingType; + ThrowIfFailed(JsGetValueType(existingGlobalThis, &existingType)); + if (existingType == JsUndefined) + { + ThrowIfFailed(JsSetProperty(global, propertyId, global, true)); + } ThrowIfFailed(JsGetPropertyIdFromName(L"Object", &propertyId)); JsValueRef object; ThrowIfFailed(JsGetProperty(global, propertyId, &object)); From fef8cb97602e2d214d680aeedf9be29babbb376e Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 14:01:49 -0500 Subject: [PATCH 2/6] Address review: define _GNU_SOURCE before system headers, bound the fallback stack limit by the thread's own size, define globalThis non-enumerable via JsDefineProperty --- Core/AppRuntime/Source/AppRuntime_QuickJS.cpp | 15 +++++++++++--- Core/Node-API/Source/env_chakra.cc | 20 ++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp index d9ab764c..b1828b7f 100644 --- a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp +++ b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp @@ -1,3 +1,8 @@ +// pthread_getattr_np is declared by glibc only under the GNU feature set; gnu++20 predefines it, +// but this translation unit should not depend on the language dialect for a system declaration. +#if defined(__linux__) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif #include "AppRuntime.h" #include @@ -43,7 +48,7 @@ namespace Babylon size_t JavaScriptStackLimit() { constexpr size_t Margin{256 * 1024}; - constexpr size_t Fallback{512 * 1024}; + constexpr size_t Fallback{256 * 1024}; size_t threadStack{}; #if defined(_WIN32) ULONG_PTR low{}; @@ -66,9 +71,13 @@ namespace Babylon pthread_attr_destroy(&attributes); } #endif - if (threadStack <= Margin) + if (threadStack == 0) + { + return Fallback; // unknown: conservative, well under any plausible thread + } + if (threadStack <= 2 * Margin) { - return Fallback; + return threadStack / 2; // a known small stack must not get a limit larger than itself } return std::min(threadStack - Margin, static_cast(JS_DEFAULT_STACK_SIZE) * 8); } diff --git a/Core/Node-API/Source/env_chakra.cc b/Core/Node-API/Source/env_chakra.cc index 641467df..1351525e 100644 --- a/Core/Node-API/Source/env_chakra.cc +++ b/Core/Node-API/Source/env_chakra.cc @@ -34,7 +34,25 @@ namespace Napi ThrowIfFailed(JsGetValueType(existingGlobalThis, &existingType)); if (existingType == JsUndefined) { - ThrowIfFailed(JsSetProperty(global, propertyId, global, true)); + // { value: globalThis, writable: true, enumerable: false, configurable: true } -- the + // spec's own data property; plain assignment would make it enumerable. + JsValueRef descriptor; + ThrowIfFailed(JsCreateObject(&descriptor)); + JsValueRef trueValue; + ThrowIfFailed(JsGetTrueValue(&trueValue)); + JsValueRef falseValue; + ThrowIfFailed(JsGetFalseValue(&falseValue)); + JsPropertyIdRef descriptorPropertyId; + ThrowIfFailed(JsGetPropertyIdFromName(L"value", &descriptorPropertyId)); + ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, global, true)); + ThrowIfFailed(JsGetPropertyIdFromName(L"writable", &descriptorPropertyId)); + ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, trueValue, true)); + ThrowIfFailed(JsGetPropertyIdFromName(L"enumerable", &descriptorPropertyId)); + ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, falseValue, true)); + ThrowIfFailed(JsGetPropertyIdFromName(L"configurable", &descriptorPropertyId)); + ThrowIfFailed(JsSetProperty(descriptor, descriptorPropertyId, trueValue, true)); + bool defined; + ThrowIfFailed(JsDefineProperty(global, propertyId, descriptor, &defined)); } ThrowIfFailed(JsGetPropertyIdFromName(L"Object", &propertyId)); JsValueRef object; From 5cd790d5f628e1eb13057a1006260e91dc8a3dfb Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 14:09:10 -0500 Subject: [PATCH 3/6] QuickJS: keep from defining min/max (Win32 QuickJS build) --- Core/AppRuntime/Source/AppRuntime_QuickJS.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp index b1828b7f..621f6310 100644 --- a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp +++ b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp @@ -27,6 +27,9 @@ #include #endif #if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX // would otherwise define min/max macros that break std::min below +#endif #include #endif @@ -79,7 +82,7 @@ namespace Babylon { return threadStack / 2; // a known small stack must not get a limit larger than itself } - return std::min(threadStack - Margin, static_cast(JS_DEFAULT_STACK_SIZE) * 8); + return (std::min)(threadStack - Margin, static_cast(JS_DEFAULT_STACK_SIZE) * 8); } } From d1ca32e239b60f5b71e20e0656f673c10fa711ba Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 14:11:56 -0500 Subject: [PATCH 4/6] QuickJS: measure the JS stack budget from the current stack pointer to the thread's base (the nominal size under-budgeted the Android runtime thread: depth-128 recursion hit the limit) --- Core/AppRuntime/Source/AppRuntime_QuickJS.cpp | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp index 621f6310..b2757f0c 100644 --- a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp +++ b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp @@ -48,41 +48,49 @@ namespace Babylon // (SIGSEGV) before QuickJS can raise "InternalError: stack overflow". Derive the limit // from the thread that actually runs the runtime instead, keeping a margin for the native // frames QuickJS and the host add between the check and the guard page. + // Bytes of C stack below the current frame, measured from the actual stack pointer to the + // thread's stack base, minus a margin for the native frames QuickJS and the host add between + // the check and the guard page. Measuring from the current position (rather than trusting + // the nominal size) also absorbs whatever the host already consumed above this call. size_t JavaScriptStackLimit() { - constexpr size_t Margin{256 * 1024}; + constexpr size_t Margin{96 * 1024}; constexpr size_t Fallback{256 * 1024}; - size_t threadStack{}; + volatile char marker{}; // the address of a local is a portable stack-pointer proxy (MSVC has no __builtin_frame_address) + const auto here = reinterpret_cast(&marker); + uintptr_t base{}; #if defined(_WIN32) ULONG_PTR low{}; ULONG_PTR high{}; GetCurrentThreadStackLimits(&low, &high); - threadStack = static_cast(high - low); + base = static_cast(low); #elif defined(__APPLE__) - // pthread_getattr_np is a GNU/bionic extension; Apple exposes the size directly. - threadStack = pthread_get_stacksize_np(pthread_self()); + // pthread_getattr_np is a GNU/bionic extension; Apple exposes the bounds directly. + const auto top = reinterpret_cast(pthread_get_stackaddr_np(pthread_self())); + base = top - pthread_get_stacksize_np(pthread_self()); #else pthread_attr_t attributes; if (pthread_getattr_np(pthread_self(), &attributes) == 0) { - void* base{}; + void* address{}; size_t size{}; - if (pthread_attr_getstack(&attributes, &base, &size) == 0) + if (pthread_attr_getstack(&attributes, &address, &size) == 0) { - threadStack = size; + base = reinterpret_cast(address); } pthread_attr_destroy(&attributes); } #endif - if (threadStack == 0) + if (base == 0 || here <= base) { return Fallback; // unknown: conservative, well under any plausible thread } - if (threadStack <= 2 * Margin) + const size_t usable = here - base; + if (usable <= 2 * Margin) { - return threadStack / 2; // a known small stack must not get a limit larger than itself + return usable / 2; // a known small stack must not get a limit larger than itself } - return (std::min)(threadStack - Margin, static_cast(JS_DEFAULT_STACK_SIZE) * 8); + return (std::min)(usable - Margin, static_cast(JS_DEFAULT_STACK_SIZE) * 8); } } From 6e094fafc65af22ac5476e0b753f41401db651dc Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 15:24:19 -0500 Subject: [PATCH 5/6] QuickJS: run the Android environment on a nested 8 MiB-stack thread instead of clamping the recursion limit The prior fix measured the worker thread's stack and lowered JS_SetMaxStackSize below it to convert the guard-page SIGSEGV into a catchable error -- but bionic's ~1 MiB worker stack leaves no budget that both avoids the guard page and admits the call depths every other engine accepts (macOS QuickJS clears depth-128 on a 512 KiB secondary-thread stack), so depth-128 recursion started failing with 'Maximum call stack size exceeded'. Instead, run the QuickJS environment on a nested thread with an 8 MiB stack (parity with the desktop threads) and keep QuickJS's own default 1 MiB limit, which now sits safely below the guard page while leaving ordinary recursion room. Confined to the QuickJS backend; other engines and platforms are unchanged. --- Core/AppRuntime/Source/AppRuntime_QuickJS.cpp | 151 ++++++++---------- 1 file changed, 67 insertions(+), 84 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp index b2757f0c..32f1832f 100644 --- a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp +++ b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp @@ -1,8 +1,3 @@ -// pthread_getattr_np is declared by glibc only under the GNU feature set; gnu++20 predefines it, -// but this translation unit should not depend on the language dialect for a system declaration. -#if defined(__linux__) && !defined(_GNU_SOURCE) -#define _GNU_SOURCE -#endif #include "AppRuntime.h" #include @@ -23,107 +18,95 @@ #pragma warning(pop) #endif -#if !defined(_WIN32) +#include + +#if defined(__ANDROID__) #include -#endif -#if defined(_WIN32) -#ifndef NOMINMAX -#define NOMINMAX // would otherwise define min/max macros that break std::min below -#endif -#include +#include +#include +#include #endif -#include -#include -#include - namespace Babylon { namespace { - // QuickJS guards against JS recursion by comparing the C stack pointer against - // stack_top - stack_size, where stack_size defaults to JS_DEFAULT_STACK_SIZE (1 MiB in - // quickjs-ng). That is also the default size of a non-main thread on Android and Windows, - // so on those threads the limit sits below the real guard page: deep recursion faults - // (SIGSEGV) before QuickJS can raise "InternalError: stack overflow". Derive the limit - // from the thread that actually runs the runtime instead, keeping a margin for the native - // frames QuickJS and the host add between the check and the guard page. - // Bytes of C stack below the current frame, measured from the actual stack pointer to the - // thread's stack base, minus a margin for the native frames QuickJS and the host add between - // the check and the guard page. Measuring from the current position (rather than trusting - // the nominal size) also absorbs whatever the host already consumed above this call. - size_t JavaScriptStackLimit() + // Runs the QuickJS environment on the calling thread. QuickJS's interpreter recurses in C + // (one JS_CallInternal frame per JS call), so deep JS call stacks need a comparably deep C + // stack; QuickJS's own limit (JS_DEFAULT_STACK_SIZE, 1 MiB) guards against overrun. + void RunQuickJSEnvironment(AppRuntime& appRuntime, void (AppRuntime::*run)(Napi::Env)) { - constexpr size_t Margin{96 * 1024}; - constexpr size_t Fallback{256 * 1024}; - volatile char marker{}; // the address of a local is a portable stack-pointer proxy (MSVC has no __builtin_frame_address) - const auto here = reinterpret_cast(&marker); - uintptr_t base{}; -#if defined(_WIN32) - ULONG_PTR low{}; - ULONG_PTR high{}; - GetCurrentThreadStackLimits(&low, &high); - base = static_cast(low); -#elif defined(__APPLE__) - // pthread_getattr_np is a GNU/bionic extension; Apple exposes the bounds directly. - const auto top = reinterpret_cast(pthread_get_stackaddr_np(pthread_self())); - base = top - pthread_get_stacksize_np(pthread_self()); -#else - pthread_attr_t attributes; - if (pthread_getattr_np(pthread_self(), &attributes) == 0) + JSRuntime* runtime = JS_NewRuntime(); + if (!runtime) { - void* address{}; - size_t size{}; - if (pthread_attr_getstack(&attributes, &address, &size) == 0) - { - base = reinterpret_cast(address); - } - pthread_attr_destroy(&attributes); + throw std::runtime_error{"Failed to create QuickJS runtime"}; } -#endif - if (base == 0 || here <= base) + + JSContext* context = JS_NewContext(runtime); + if (!context) { - return Fallback; // unknown: conservative, well under any plausible thread + JS_FreeRuntime(runtime); + throw std::runtime_error{"Failed to create QuickJS context"}; } - const size_t usable = here - base; - if (usable <= 2 * Margin) + { - return usable / 2; // a known small stack must not get a limit larger than itself + Napi::Env env = Napi::Attach(context); + (appRuntime.*run)(env); + Napi::Detach(env); } - return (std::min)(usable - Margin, static_cast(JS_DEFAULT_STACK_SIZE) * 8); + + JS_FreeContext(context); + JS_FreeRuntime(runtime); } } void AppRuntime::RunEnvironmentTier(const char* /*executablePath*/) { - // Create the runtime. - JSRuntime* runtime = JS_NewRuntime(); - if (!runtime) - { - throw std::runtime_error{"Failed to create QuickJS runtime"}; - } - JS_SetMaxStackSize(runtime, JavaScriptStackLimit()); - - // Create the context. - JSContext* context = JS_NewContext(runtime); - if (!context) - { - JS_FreeRuntime(runtime); - throw std::runtime_error{"Failed to create QuickJS context"}; - } +#if defined(__ANDROID__) + // bionic gives this worker thread ~1 MiB of stack, at or below QuickJS's default 1 MiB + // recursion limit -- so deep-but-legal JS recursion faults the guard page (SIGSEGV) before + // QuickJS can raise a catchable "stack overflow", while clamping the limit below 1 MiB + // instead rejects call depths that every other engine (and desktop QuickJS on its ~8 MiB + // stack) accepts. Run the environment on a nested thread with a desktop-sized stack so + // QuickJS's own default limit sits safely below the guard page and ordinary recursion fits. + std::function body{[this] { RunQuickJSEnvironment(*this, &AppRuntime::Run); }}; + std::exception_ptr thrown{}; + auto payload = std::make_pair(&body, &thrown); + auto trampoline = [](void* arg) -> void* { + auto* p = static_cast*, std::exception_ptr*>*>(arg); + try + { + (*p->first)(); + } + catch (...) + { + *p->second = std::current_exception(); + } + return nullptr; + }; - // Use the context within a scope. + pthread_attr_t attr; + if (pthread_attr_init(&attr) == 0) { - Napi::Env env = Napi::Attach(context); - - Run(env); - - Napi::Detach(env); + pthread_attr_setstacksize(&attr, 8 * 1024 * 1024); + pthread_t tid{}; + const int created = pthread_create(&tid, &attr, trampoline, &payload); + pthread_attr_destroy(&attr); + if (created == 0) + { + pthread_join(tid, nullptr); + if (thrown) + { + std::rethrow_exception(thrown); + } + return; + } } - - // Destroy the context and runtime. - JS_FreeContext(context); - JS_FreeRuntime(runtime); + // Thread creation failed: fall back to the current thread. + RunQuickJSEnvironment(*this, &AppRuntime::Run); +#else + RunQuickJSEnvironment(*this, &AppRuntime::Run); +#endif } void AppRuntime::ShutdownEnvironment(Napi::Env) From c502dcb2c2d9d9fa1a9d8936bdfd35bd4736a189 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 15:29:30 -0500 Subject: [PATCH 6/6] QuickJS: raise the nested-thread JS stack limit to 6 MiB for deep recursion (Debug frames) The nested 8 MiB thread removed the guard-page SIGSEGV but QuickJS's default 1 MiB limit still rejected depth-128 recursion in unoptimized Debug builds (large JS_CallInternal frames). Set JS_SetMaxStackSize to 6 MiB on the guaranteed-8 MiB nested thread; default elsewhere. Emulator-validated: Android QuickJS 30/30. --- Core/AppRuntime/Source/AppRuntime_QuickJS.cpp | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp index 32f1832f..ca219021 100644 --- a/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp +++ b/Core/AppRuntime/Source/AppRuntime_QuickJS.cpp @@ -19,6 +19,7 @@ #endif #include +#include #if defined(__ANDROID__) #include @@ -34,13 +35,20 @@ namespace Babylon // Runs the QuickJS environment on the calling thread. QuickJS's interpreter recurses in C // (one JS_CallInternal frame per JS call), so deep JS call stacks need a comparably deep C // stack; QuickJS's own limit (JS_DEFAULT_STACK_SIZE, 1 MiB) guards against overrun. - void RunQuickJSEnvironment(AppRuntime& appRuntime, void (AppRuntime::*run)(Napi::Env)) + // jsStackLimit: value for JS_SetMaxStackSize, or 0 to keep QuickJS's default (1 MiB). A + // non-default limit is only safe when the caller guarantees a stack large enough to hold it + // below the guard page -- see the Android nested-thread path below. + void RunQuickJSEnvironment(AppRuntime& appRuntime, void (AppRuntime::*run)(Napi::Env), size_t jsStackLimit) { JSRuntime* runtime = JS_NewRuntime(); if (!runtime) { throw std::runtime_error{"Failed to create QuickJS runtime"}; } + if (jsStackLimit != 0) + { + JS_SetMaxStackSize(runtime, jsStackLimit); + } JSContext* context = JS_NewContext(runtime); if (!context) @@ -68,8 +76,14 @@ namespace Babylon // QuickJS can raise a catchable "stack overflow", while clamping the limit below 1 MiB // instead rejects call depths that every other engine (and desktop QuickJS on its ~8 MiB // stack) accepts. Run the environment on a nested thread with a desktop-sized stack so - // QuickJS's own default limit sits safely below the guard page and ordinary recursion fits. - std::function body{[this] { RunQuickJSEnvironment(*this, &AppRuntime::Run); }}; + // QuickJS's raised limit sits safely below the guard page and deep recursion fits. The + // stack must be generous because connectedAndroidTest is an unoptimized Debug build, whose + // JS_CallInternal frames are several times larger than a release build's -- depth-128 + // recursion (which release QuickJS clears within the 1 MiB default) needs well over 1 MiB + // here, so QuickJS's default limit would still reject it on any thread size. + constexpr size_t NestedStackSize{8 * 1024 * 1024}; + constexpr size_t JsStackLimit{6 * 1024 * 1024}; // < NestedStackSize guard; > debug depth-128 need + std::function body{[this] { RunQuickJSEnvironment(*this, &AppRuntime::Run, JsStackLimit); }}; std::exception_ptr thrown{}; auto payload = std::make_pair(&body, &thrown); auto trampoline = [](void* arg) -> void* { @@ -88,7 +102,7 @@ namespace Babylon pthread_attr_t attr; if (pthread_attr_init(&attr) == 0) { - pthread_attr_setstacksize(&attr, 8 * 1024 * 1024); + pthread_attr_setstacksize(&attr, NestedStackSize); pthread_t tid{}; const int created = pthread_create(&tid, &attr, trampoline, &payload); pthread_attr_destroy(&attr); @@ -102,10 +116,11 @@ namespace Babylon return; } } - // Thread creation failed: fall back to the current thread. - RunQuickJSEnvironment(*this, &AppRuntime::Run); + // Thread creation failed: fall back to the current (small) worker thread, where only + // QuickJS's default limit is safe. + RunQuickJSEnvironment(*this, &AppRuntime::Run, 0); #else - RunQuickJSEnvironment(*this, &AppRuntime::Run); + RunQuickJSEnvironment(*this, &AppRuntime::Run, 0); #endif }