You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 27, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Oct 27, 2026. It is now read-only.
JavaScriptCore execution-time-limit watchdog only polls once per VM entry (WebKit Watchdog bug; worked around by re-arming from the callback) #241
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)
Expected: callback 1, callback 2, callback 3, then JSEvaluateScript returns with the termination exception after ~150 ms.
Actual: callback 1 only; the script runs forever.
With callback = NULL the 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:
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).
I can't reopen this from my side (the API refuses the state change for a non-collaborator author, association NONE). @bghgary, would you mind reopening it now that #258 is up? The stack above is the full context.
Activity
bghgary commentedon Sep 17, 2026
[Commented by Copilot on behalf of @bghgary]
Closing this until the Worker implementation is opened for review in BabylonJS/JsRuntimeHost. Please reopen it when the Worker PR is ready.
matthargett commentedon Sep 26, 2026
Reopening as requested: the Worker implementation is open for review as #258.
The stack, bottom-up (each PR's diff includes the ones below it until they merge); every PR in it is current with
main@ b2b51ca and mergeable:data:fetch.ExecutionWatchdog::RearminCore/AppRuntime/Source/AppRuntime_JavaScriptCore.cppthere, exercised by the WPT terminate-during-evaluation regression on JavaScriptCore.Review/merge order is the same.
matthargett commentedon Sep 26, 2026
I can't reopen this from my side (the API refuses the state change for a non-collaborator author, association NONE). @bghgary, would you mind reopening it now that #258 is up? The stack above is the full context.