From 155b04c1fa196dd0d3586a826914768ca16a1486 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:15:04 +0000 Subject: [PATCH] Make helper-thread spawn failures non-fatal Add WTF::Thread::tryCreate (returns nullptr instead of RELEASE_ASSERT when pthread_create/_beginthreadex is refused) and route the process-survivable callers through it: - AutomaticThread::start: on failure, stay in the no-underlying-thread state so the next notify retries. Covers the GC marker pool (ParallelHelperPool), the JIT/Wasm worklists, and the per-VM Heap collector thread. - Heap::finishRelinquishingConn: if the collector thread could not be spawned, take the conn back so the mutator drives the collection instead of parking forever in waitForCollector. - pas_scavenger: on pthread_create failure, drop back to the no-thread state and let the next eligibility notification retry instead of PAS_ASSERTing. - VMTraps: create the signal-sender WorkQueue eagerly in initializeSignals so it is spawned while the process still has thread budget rather than on the first fireTrap. A container that caps pids.max (or a user at RLIMIT_NPROC) can hit pthread_create EAGAIN on any of these when an embedder churns VMs; the process now degrades (slower GC/JIT, fewer helpers) instead of aborting. --- Source/JavaScriptCore/heap/Heap.cpp | 9 ++++++++- Source/JavaScriptCore/runtime/VMTraps.cpp | 6 ++++++ Source/WTF/wtf/AutomaticThread.cpp | 12 ++++++++++-- Source/WTF/wtf/Threading.cpp | 14 ++++++++++++-- Source/WTF/wtf/Threading.h | 6 +++++- Source/bmalloc/libpas/src/libpas/pas_scavenger.c | 9 +++++++-- 6 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Source/JavaScriptCore/heap/Heap.cpp b/Source/JavaScriptCore/heap/Heap.cpp index decf60d0df21..4a3f7058d07d 100644 --- a/Source/JavaScriptCore/heap/Heap.cpp +++ b/Source/JavaScriptCore/heap/Heap.cpp @@ -2257,8 +2257,15 @@ void Heap::finishRelinquishingConn() sanitizeStackForVM(vm()); Locker locker { *m_threadLock }; - if (!m_requests.isEmpty()) + if (!m_requests.isEmpty()) { m_threadCondition->notifyOne(locker); + if (!m_thread->hasUnderlyingThread(locker)) [[unlikely]] { + // The collector thread couldn't spawn (pthread_create EAGAIN at a + // pids/thread limit). Take the conn back so the mutator drives the + // collection; otherwise waitForCollector() would park forever. + m_worldState.exchangeOr(mutatorHasConnBit); + } + } ParkingLot::unparkAll(&m_worldState); } diff --git a/Source/JavaScriptCore/runtime/VMTraps.cpp b/Source/JavaScriptCore/runtime/VMTraps.cpp index 7dd488259b83..e7c9f49f77e3 100644 --- a/Source/JavaScriptCore/runtime/VMTraps.cpp +++ b/Source/JavaScriptCore/runtime/VMTraps.cpp @@ -366,6 +366,12 @@ void VMTraps::initializeSignals() if (!Options::usePollingTraps()) { ASSERT(Options::useJIT()); SignalSender::initializeSignals(); + // Create the signal-sender WorkQueue (and its backing thread) now, + // while the process still has thread-budget headroom, instead of on + // the first fireTrap() which may arrive after workers have consumed + // every slot of a cgroup pids.max budget. RunLoop::create() cannot + // recover from a failed Thread::create. + queue(); } #endif } diff --git a/Source/WTF/wtf/AutomaticThread.cpp b/Source/WTF/wtf/AutomaticThread.cpp index b683935d0713..cfeceef500fa 100644 --- a/Source/WTF/wtf/AutomaticThread.cpp +++ b/Source/WTF/wtf/AutomaticThread.cpp @@ -180,7 +180,7 @@ void AutomaticThread::start(const AbstractLocker&) break; } - Thread::create( + RefPtr thread = Thread::tryCreate( name(), [=, this] () { if (verbose) @@ -245,7 +245,15 @@ void AutomaticThread::start(const AbstractLocker&) } RELEASE_ASSERT(result == WorkResult::Continue); } - }, m_threadType, Thread::defaultQOS, Thread::defaultSchedulingPolicy, stackSpec)->detach(); + }, m_threadType, Thread::defaultQOS, Thread::defaultSchedulingPolicy, stackSpec); + if (!thread) { + // pthread_create/_beginthreadex refused (e.g. EAGAIN at a thread/pids + // limit). Stay in the no-underlying-thread state so the next notify + // retries instead of bringing the whole process down. + m_hasUnderlyingThread = false; + return; + } + thread->detach(); } void AutomaticThread::threadDidStart() diff --git a/Source/WTF/wtf/Threading.cpp b/Source/WTF/wtf/Threading.cpp index 2438507b7a14..a7d1c2ea2de8 100644 --- a/Source/WTF/wtf/Threading.cpp +++ b/Source/WTF/wtf/Threading.cpp @@ -313,6 +313,13 @@ void Thread::entryPoint(NewThreadContext* newThreadContext) } Ref Thread::create(ASCIILiteral name, Function&& entryPoint, ThreadType threadType, QOS qos, SchedulingPolicy schedulingPolicy, StackAllocationSpecification stackSpec) +{ + RefPtr thread = tryCreate(name, WTF::move(entryPoint), threadType, qos, schedulingPolicy, stackSpec); + RELEASE_ASSERT_WITH_MESSAGE(thread, "Failed to create thread '%s'", name.characters()); + return thread.releaseNonNull(); +} + +RefPtr Thread::tryCreate(ASCIILiteral name, Function&& entryPoint, ThreadType threadType, QOS qos, SchedulingPolicy schedulingPolicy, StackAllocationSpecification stackSpec) { WTF::initialize(); @@ -327,8 +334,11 @@ Ref Thread::create(ASCIILiteral name, Function&& entryPoint, Thr if (maybeSize) stackSpec = StackAllocationSpecification::RequestSize(maybeSize.value()); } - bool success = thread->establishHandle(context.get(), stackSpec, qos, schedulingPolicy); - RELEASE_ASSERT(success); + if (!thread->establishHandle(context.get(), stackSpec, qos, schedulingPolicy)) { + // Undo the speculative +1 for Thread::entryPoint; the locker's Ref drops the rest. + context->deref(); + return nullptr; + } #if HAVE(STACK_BOUNDS_FOR_NEW_THREAD) thread->m_stack = StackBounds::newThreadStackBounds(thread->m_handle); diff --git a/Source/WTF/wtf/Threading.h b/Source/WTF/wtf/Threading.h index c7980a9fceca..ddbc950b5aca 100644 --- a/Source/WTF/wtf/Threading.h +++ b/Source/WTF/wtf/Threading.h @@ -134,10 +134,14 @@ class WTF_CAPABILITY("is current") Thread : public ThreadSafeRefCountedAndCanMak static dispatch_qos_class_t dispatchQOSClass(QOS); #endif - // Returns nullptr if thread creation failed. // The thread name must be a literal since on some platforms it's passed in to the thread. + // Aborts if the OS refuses to create the thread (resource limit, etc.). WTF_EXPORT_PRIVATE static Ref create(ASCIILiteral threadName, Function&&, ThreadType = ThreadType::Unknown, QOS = defaultQOS, SchedulingPolicy = defaultSchedulingPolicy, StackAllocationSpecification = { }); + // Returns nullptr if thread creation failed. The caller still owns entryPoint + // resources in that case (the moved Function is destroyed, its captures drop). + WTF_EXPORT_PRIVATE static RefPtr tryCreate(ASCIILiteral threadName, Function&&, ThreadType = ThreadType::Unknown, QOS = defaultQOS, SchedulingPolicy = defaultSchedulingPolicy, StackAllocationSpecification = { }); + // Returns Thread object. static Thread& currentSingleton(); diff --git a/Source/bmalloc/libpas/src/libpas/pas_scavenger.c b/Source/bmalloc/libpas/src/libpas/pas_scavenger.c index 2ad30036b38a..39588a2e97be 100644 --- a/Source/bmalloc/libpas/src/libpas/pas_scavenger.c +++ b/Source/bmalloc/libpas/src/libpas/pas_scavenger.c @@ -521,8 +521,13 @@ void pas_scavenger_notify_eligibility_if_needed(void) int result; pas_scavenger_current_state = pas_scavenger_state_polling; result = pthread_create(&thread, NULL, scavenger_thread_main, NULL); - PAS_ASSERT(!result); - pthread_detach(thread); + if (PAS_LIKELY(!result)) + pthread_detach(thread); + else { + /* EAGAIN at a thread/pids limit: leave memory unscavenged rather + than killing the process. The next did_create_eligible retries. */ + pas_scavenger_current_state = pas_scavenger_state_no_thread; + } } if (pas_scavenger_current_state == pas_scavenger_state_deep_sleep) {