Skip to content

Bump WebKit so a refused JIT, GC, Wasm or VMTraps thread does not abort the process - #46265

Draft
robobun wants to merge 4 commits into
mainfrom
robobun/709f24c7/thread-limit-no-abort
Draft

robobun wants to merge 4 commits into
mainfrom
robobun/709f24c7/thread-limit-no-abort

Conversation

@robobun

@robobun robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #44787

Draft until oven-sh/WebKit#824 lands on its main. WEBKIT_VERSION then moves from the preview tarball to the merged sha. Supersedes #41587 (the same bump for the earlier oven-sh/WebKit#569).

Problem

  • A process at its cgroup pids.max or RLIMIT_NPROC limit dies with panic(main thread): abort() called as soon as a loop gets hot, on its next GC, on new WebAssembly.Module, or on a node:vm timeout. An idle script that polls with setTimeout dies too. Node reports EAGAIN and keeps running.
  • The cause is in WebKit: WTF::Thread::create has RELEASE_ASSERT(success) on pthread_create EAGAIN (JITWorklist::enqueue -> wakeThreads -> AutomaticThread::start -> Thread::create). Bun's own spawn paths already report EAGAIN.

Fix

  • Bump WEBKIT_VERSION to the preview build of Survive a refused thread: GC helpers, collector, JIT worklist, VMTraps and libpas retry instead of crashing WebKit#824. Thread creation is fallible there (Thread::tryCreate, an AutomaticThreadCondition::StartFailure::Retry policy). The JIT and Wasm worklists compile on the calling thread when no compiler thread can start, the GC collector's marking runs on the mutator when no collector thread can start, the VMTraps sender and the deadline timers wait for a thread instead of asserting, and the libpas scavenger retries later.
  • Verified: test/js/bun/jsc/thread-limit.test.ts. Four cases under runuser -u nobody -- prlimit --nproc=1 with the concurrent JIT and 4 GC markers on: a hot loop, an async then a sync GC, WebAssembly.validate + new WebAssembly.Module, vm.runInNewContext with a timeout. Each first proves the limit is in force (spawnSync("true") reports EAGAIN). All four abort with the current WEBKIT_VERSION (USE_SYSTEM_BUN=1) and pass with the bump. Also test/js/bun/jsc, test/js/bun/wasm, test/js/bun/util/bun-gc.test.ts, test/js/web/workers/worker.test.ts, test/js/node/vm/vm.test.ts.

Background

  • On Linux every thread counts toward RLIMIT_NPROC, and a cgroup v2 pids.max counts threads too. Root is exempt from RLIMIT_NPROC, so the test drops to nobody through runuser and sets the limit with prlimit --nproc. It skips where those are missing.
  • JSC starts most of its threads on demand and lets them exit when idle: the JIT worklist thread on the first hot function, the collector thread when the mutator hands off a collection, the parallel markers on a mark phase, the VMTraps sender on the first trap. So a limit reached after startup (the report's case: a sibling process used the budget) refuses the next re-creation.
  • bun -e turns the concurrent JIT off and marks with one thread, so the test sets BUN_JSC_useConcurrentJIT=1 and BUN_JSC_numberOfGCMarkers=4 to ask for the threads a script run from a file asks for.
  • Considered a fix in bun's __wrap_pthread_create (src/jsc/bindings/c-bindings.cpp): it cannot create a thread the OS refuses, a retry only masks a transient limit. Considered turning the concurrent JIT off at startup: it cannot react to a limit reached later, and the collector thread and Wasm worklist are refused the same way. The WTF layer is where every lazy JSC thread converges.

Downsides

  • Processes that never hit the limit pay one relaxed atomic load per AutomaticThread::start (a thread start, rare) and one per mutator-conducted concurrent-phase poll in Collector::runConcurrentPhase. VMTraps::tryQueue takes a lock per trap notify where a call_once was. No allocations, no syscalls added. Binary size is not comparable until WEBKIT_VERSION points at a release build: the preview tarball is built without the release flags.
  • Under the limit a process degrades: JIT compiles run on the main thread, a GC's marking runs on the main thread, a node:vm timeout is not enforced until a thread can start, and worker.terminate() of optimized code with no calls waits for a later trap.
  • Still aborts under the limit: the sampling profiler's thread (Bun.CPUProfile, --cpu-prof). Still hangs: bun's own thread pool for async fs (thread pool: report a pool that cannot spawn any worker instead of hanging #37738) and new Worker (Worker: throw ERR_WORKER_INIT_FAILED when the OS refuses the thread #41675), separate fixes.
Notes

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/jsc/thread-limit.test.ts

@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:28 PM PT - Oct 20th, 2026

❌ @robobun, your commit 5e9dbb0 has 2 failures in Build #129747 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 46265

That installs a local version of the PR into your bun-46265 executable, so you can run:

bun-46265 --bun

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bun aborts (panic: abort() called) when it cannot create a thread (EAGAIN), even while idle; Node reports spawn EAGAIN as an error

1 participant