Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

JavaScriptCore execution-time-limit watchdog only polls once per VM entry (WebKit Watchdog bug; worked around by re-arming from the callback)

オープン
#241 コメント 3 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
42/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
cpp, javascript
領域
backend

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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, 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:

    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).

主要言語
C++
スター
22
フォーク
23
平均マージ
6時間 17分
マージ済み PR(30日)
4

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

BabylonJS/JsRuntimeHost のほかの issue

BabylonJS/JsRuntimeHost の issue をすべて見る

似ている issue

C++ の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。