JavaScriptCore execution-time-limit watchdog only polls once per VM entry (WebKit Watchdog bug; worked around by re-arming from the callback)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 42/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- cpp, javascript
- Domain
- backend
Research direction
Read Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp, especially the Worker branch's ExecutionWatchdog::Rearm, then compare it with WebKit's Source/JavaScriptCore/runtime/Watchdog.cpp and JSContextRefPrivate.h. Run the standalone JavaScriptCore reproduction to confirm the single-callback behavior. Done means the engine behavior is tracked with WebKit and the required re-arm workaround remains documented and intact.
Written by the indexing model from the issue text.
Description
Summary
JavaScriptCore's execution-time-limit watchdog (JSContextGroupSetExecutionTimeLimit, JSContextRefPrivate.h) fires its callback once per VM entry when the callback returns false: Watchdog::shouldTerminate() never restarts the timer in its "callback did nothing" case. Any embedder that polls from that callback — which is how JsRuntimeHost's Worker polyfill lets Worker.terminate() stop a while (true) {} — gets exactly one poll and then a script that can no longer be interrupted.
This is a latent WebKit bug, not something in this repository. JsRuntimeHost works around it by re-setting the limit from inside the callback (the WebKit comment's "case 2"), which restarts the timer through setTimeLimit(). The workaround lives in Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp on the Worker branch (ExecutionWatchdog::Rearm, fork commit rebeckerspecialties/JsRuntimeHost@c6f8afd) and will arrive here with the Worker polyfill PR. This issue records the engine behaviour so the re-arm is not "simplified" away later, and so the WebKit report can be tracked.
Where it goes wrong (WebKit Source/JavaScriptCore/runtime/Watchdog.cpp)
// 1. cleared the time limit (i.e. watchdog is disabled),
// 2. set a new time limit via Watchdog::setTimeLimit(), or
// 3. did nothing (i.e. allow another cycle of the current time limit).
...
bool callbackAlreadyStartedTimer = (m_cpuDeadline != noTimeLimit);
if (hasTimeLimit() && !callbackAlreadyStartedTimer)
startTimer(m_timeLimit);
m_cpuDeadline is only reset to noTimeLimit by stopTimer() (called from exitedVM()). When shouldTerminate() runs after the CPU deadline expired, m_cpuDeadline still holds that expired deadline, so callbackAlreadyStartedTimer is true and case 3 schedules nothing. JSContextRefPrivate.h documents the callback as deciding whether to terminate, not as being responsible for re-arming.
Reproduction (standalone, against the system JavaScriptCore on macOS 27 / Darwin 27.0)
static int calls = 0;
static bool cb(JSContextRef ctx, void* data) { fprintf(stderr, "callback %d\n", ++calls); return calls >= 3; }
...
JSGlobalContextRef ctx = JSGlobalContextCreateInGroup(NULL, NULL);
JSContextGroupSetExecutionTimeLimit(JSContextGetGroup(ctx), 0.05, cb, NULL);
JSEvaluateScript(ctx, JSStringCreateWithUTF8CString("while (true) {}"), NULL, NULL, 1, &exception);
- Expected:
callback 1,callback 2,callback 3, thenJSEvaluateScriptreturns with the termination exception after ~150 ms. - Actual:
callback 1only; the script runs forever. - With
callback = NULLthe script is terminated after 50 ms as documented, and re-setting the limit from inside the callback also works — which is the workaround.
Bun's Linux/Android JavaScriptCore archives (#206) carry the same code, so the behaviour is engine-wide, not Apple-specific. (Under ThreadSanitizer on Linux the trap that carries the termination is additionally undeliverable — signal-based VM traps — which is why the fork's TSan job sets JSC_usePollingTraps=1; that is a separate, TSan-only effect.)
Suggested WebKit fix
Clear m_cpuDeadline before invoking the callback so "callback started a timer" is only true when the callback's own setTimeLimit() → startTimer() ran:
m_cpuDeadline = noTimeLimit;
bool needsTermination = !m_callback || m_callback(globalObject, m_callbackData1, m_callbackData2);
A bugs.webkit.org report with this content is the next step (draft kept alongside the fork's integration notes); this issue tracks it from the JsRuntimeHost side.
Also worth knowing when touching this path
The termination exception the watchdog raises is a bare string ("JavaScript execution terminated.", VM::ensureTerminationException) since 2021, not an Error object; handing it to napi_create_reference used to trip a RELEASE_ASSERT inside JavaScriptCore on Apple platforms (fixed by #239).
- Dominant language
- C++
- Stars
- 22
- Forks
- 23
- Avg merge
- 4d 8h
- Merged PRs (30d)
- 3
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from BabylonJS/JsRuntimeHost
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
BabylonJS/JsRuntimeHost#234 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
BabylonJS/JsRuntimeHost#173 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
BabylonJS/JsRuntimeHost#228 ·
Maintainers usually reply within 1 day
-
napi_unwrap does not reject objects that were never wrapped (V8 port faults, QuickJS port confuses types)Possibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 4/5 3-5 days Newbie friendliness 68/100
BabylonJS/JsRuntimeHost#226 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 4/5 3-5 days Newbie friendliness 35/100
BabylonJS/JsRuntimeHost#219 ·
Maintainers usually reply within 1 day
All issues in BabylonJS/JsRuntimeHost
Similar issues
-
agent:WSL bug linux LOW ui
Difficulty 1/5 Under an hour Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Copter: PosHold brake-entry threshold became 16 deg instead of 0.16 deg after the radians conversionOpen
Difficulty 1/5 Under an hour Newbie friendliness 78/100
ArduPilot/ardupilot#34617 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
tesseract-robotics/tesseract_nanobind#168 ·
Maintainers usually reply within 1 day
-
Self-hosted runner Dockerfile pins actions/runner 2.327.1, below GitHub's new minimum (2.329.0)Openauto-triaged bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
bug needs-triage
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
microsoft/microsoft-ui-xaml#12158 ·
Maintainers usually reply within 1 day