Survive a refused thread creation in the GC helper pool, the collector thread, and the JIT worklist - #489
Draft
robobun wants to merge 1 commit into
Draft
Conversation
…r thread, and the JIT worklist WTF::Thread::create crashes when pthread_create fails. Under a thread or pid limit (RLIMIT_NPROC, a cgroup pids.max full of the process's own children) the first full GC after the limit is reached aborts the process from AutomaticThread::start, because the heap helper threads and the collector thread are AutomaticThreads that exit after 10 seconds of idle time and are created again on demand. Add Thread::tryCreate, which returns nullptr instead of crashing, and let AutomaticThreadCondition choose what a failed start means. The default (StartFailure::Crash) keeps the old behavior. With StartFailure::Retry the AutomaticThread is left without an underlying thread, the notify returns false, and the next notify tries again. ParallelHelperPool uses Retry: the client runs the task on its own thread. Its destructor stops threads that never started before it joins them. JSC::Heap uses Retry. When the collector thread cannot start, the mutator takes the conn back and runs the collection itself. A mutator that waits for a collection also runs the collector's phases, because the concurrent phase is the collector's and, with the stochastic scheduler, a waiting mutator would otherwise never see it end. JITWorklist uses Retry. wakeThreads counts only threads that were notified or started. waitUntilAllPlansForVMAreReady cancels plans that no compiler thread can take, so deleteAllCode does not wait forever. The code blocks stay in the lower tier and can enqueue again.
This was referenced Aug 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
systemd-run -p TasksMax=64 bun spawn80.js, orprlimit --nproc=64) aborts at its next full GC:ASSERTION FAILED: successinThread::create(Threading.cpp:330), reached fromAutomaticThread::startviaParallelHelperPool::didMakeWorkAvailableinHeap::runBeginPhase. Node under the same limit reportsEAGAINfor the spawns that do not fit and keeps running.AutomaticThreads. They exit after 10 seconds of idle time and are created again on demand, so every GC and every tier-up after the limit is reached is apthread_createthat can fail.Thread::createhas no failure path.Fix
Thread::tryCreatereturnsnullptrwhenestablishHandlefails.Thread::createnow wraps it and keeps the crash.AutomaticThreadCondition::create(StartFailure): withRetry, a failedAutomaticThread::startleaves the thread without an underlying thread andnotifyOne/notifyAllreturnfalse. The defaultCrashkeeps the old behavior for every other user.ParallelHelperPoolusesRetry: the client drains the work itself.JSC::HeapusesRetry: the mutator takes the conn back and, when it waits for the collection, runs the collector's phases too (seecollectAsTheCollectorBecauseCollectorThreadCouldNotStart).JITWorklistusesRetry:wakeThreadscounts only threads that exist, andwaitUntilAllPlansForVMAreReadycancels plans nobody can compile instead of waiting forever.prlimit --nproc=64as an unprivileged user: the spawn repro prints{ ok: 58, err: 22 }and exits 0 (before: abort). A 300k-object heap with sync and async GCs while the pid budget stays full completes. A hot loop that needs a compiler thread that cannot be created, followed bydeleteAllCode, returns in 48 ms and the JIT recovers once threads are available again. With no limit, 20 sync GCs take the same time as before (1150 ms vs 1152 ms), and a sample of Bun's suites has the same result set.Background
stopIfNecessarypolls) or the collector thread holds it.requestCollectionusually steals it for the mutator;relinquishConnhands it to the collector and that is where the collector thread gets created.StochasticSpaceTimeMutatorSchedulerit ends when marking terminates or when the mutator allocates its headroom. A mutator parked inwaitForCollectorallocates nothing, so without a collector that phase never ends. The patch has the waiting mutator clear its conn bit and callcollectInCollectorThread;stopTheMutatorhands the conn back as soon as the world must stop, because the mutator has heap access.JITWorklist::m_numberOfActiveThreadsis incremented per notify and decremented when a worklist thread finds no work. A notify that started no thread used to leave it too high.Notes
Fuzz ledger repro (Bun 1.4.0):
Without a cgroup,
prlimit --nproc=64 setpriv --reuid=nobody bun spawn80.jsreproduces it.Iterations of this patch and what they showed:
finishRelinquishingConnand aborted on the collector thread instead.Retrywith the mutator keeping the conn and spinning:relinquishConnloops while the conn bit is set, so it retriedpthread_createon every iteration (130k attempts in 10 s).finishRelinquishingConnnow reports the retake andrelinquishConnstops.m_scheduler->timeToStop(): with the stochastic scheduler that isinfinitywhile the mutator allocates nothing, so a sync GC never finished once the pid budget stayed full. Hence the collector stint on the mutator thread.The debug-only
LOG_ERRORinestablishHandlestill prints one line per refused thread. Release builds print nothing.Not changed:
WorkerPool,Wasm::Worklist,SequesteredAutomaticThreadkeepStartFailure::Crash. Every directThread::createcaller keeps the crash.