Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
16cf9b3
fix(workers): add WebWorkerLifecycleHandle to prevent use-after-free
robobun Mar 20, 2026
fc1066d
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 20, 2026
b60ac7e
fix: detach handle.worker before destroying WebWorker
robobun Mar 20, 2026
1fb6692
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 20, 2026
979d855
fix: null handle.worker in destroy() and use unrefConcurrently
robobun Mar 20, 2026
403354b
simplify: drop WebWorkerLifecycleHandle — put ThreadSafeRefCount dire…
alii Mar 20, 2026
18069fd
Merge branch 'main' into farm/8b799671/worker-lifecycle-handle
alii Mar 20, 2026
e5af6fa
fix: spawn failure parent ref leak, name buffer leak, updatePtr ref leak
robobun Mar 20, 2026
f47a946
fix: release Zig WebWorker ref in updatePtr failure, add double-unref…
robobun Mar 20, 2026
3cb9b1b
fix: move parent_poll_ref unref after dispatchExit, fix spawn failure…
robobun Mar 20, 2026
df4b3b7
fix: restore unref in notifyNeedTermination to prevent parent hang
robobun Mar 20, 2026
2859dc0
fix: release heap resources synchronously in exitAndDeinit
alii Mar 20, 2026
3cd267d
fix: guard releaseResources frees with length checks
robobun Mar 20, 2026
ed28231
fix: remove flaky double-terminate test
robobun Mar 20, 2026
8d7f8e7
test: add back double-terminate test (sequential, not racy)
robobun Mar 20, 2026
47161c9
test: add concurrent terminate test (fire-and-forget, no await)
robobun Mar 20, 2026
42ed136
fix(workers): keep parent keep-alive ref'd until exitAndDeinit
alii Mar 20, 2026
9c30d51
fix: explicit unrefConcurrently in spawn failure path
robobun Mar 20, 2026
d133a93
Revert "fix(workers): keep parent keep-alive ref'd until exitAndDeinit"
robobun Mar 20, 2026
d254d52
ci: retrigger build
robobun Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 27 additions & 15 deletions src/bun.js/bindings/webcore/Worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ namespace WebCore {

WTF_MAKE_TZONE_ALLOCATED_IMPL(Worker);

extern "C" void WebWorker__notifyNeedTermination(
void* worker);
extern "C" void WebWorkerLifecycleHandle__requestTermination(WebWorkerLifecycleHandle* handle);
extern "C" void WebWorkerLifecycleHandle__release(WebWorkerLifecycleHandle* handle);

static Lock allWorkersLock;
static HashMap<ScriptExecutionContextIdentifier, Worker*>& allWorkers() WTF_REQUIRES_LOCK(allWorkersLock)
Expand Down Expand Up @@ -108,8 +108,8 @@ Worker::Worker(ScriptExecutionContext& context, WorkerOptions&& options)
auto addResult = allWorkers().add(m_clientIdentifier, this);
ASSERT_UNUSED(addResult, addResult.isNewEntry);
}
extern "C" bool WebWorker__updatePtr(void* worker, Worker* ptr);
extern "C" void* WebWorker__create(
extern "C" bool WebWorker__updatePtr(WebWorkerLifecycleHandle* handle, Worker* ptr);
extern "C" WebWorkerLifecycleHandle* WebWorkerLifecycleHandle__createWebWorker(
Worker* worker,
void* parent,
BunString name,
Expand All @@ -128,17 +128,18 @@ extern "C" void* WebWorker__create(
BunString* preloadModulesPtr,
size_t preloadModulesLen);
extern "C" void WebWorker__setRef(
void* worker,
WebWorkerLifecycleHandle* handle,
bool ref);

void Worker::setKeepAlive(bool keepAlive)
{
WebWorker__setRef(impl_, keepAlive);
if (lifecycleHandle_)
WebWorker__setRef(lifecycleHandle_, keepAlive);
}

bool Worker::updatePtr()
{
if (!WebWorker__updatePtr(impl_, this)) {
if (!WebWorker__updatePtr(lifecycleHandle_, this)) {
m_onlineClosingFlags = ClosingFlag;
m_terminationFlags.fetch_or(TerminatedFlag);
return false;
Expand Down Expand Up @@ -189,7 +190,7 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
return { reinterpret_cast<WTF::StringImpl**>(vec.begin()), vec.size() };
})
.value_or(std::span<WTF::StringImpl*> {});
void* impl = WebWorker__create(
WebWorkerLifecycleHandle* lifecycleHandle = WebWorkerLifecycleHandle__createWebWorker(
worker.ptr(),
bunVM(context.jsGlobalObject()),
nameStr,
Expand All @@ -207,16 +208,16 @@ ExceptionOr<Ref<Worker>> Worker::create(ScriptExecutionContext& context, const S
execArgv.size(),
preloadModules.begin(),
preloadModules.size());
// now referenced by Zig
worker->ref();

preloadModuleStrings.clear();

if (!impl) {
if (!lifecycleHandle) {
return Exception { TypeError, errorMessage.toWTFString(BunString::ZeroCopy) };
}

worker->impl_ = impl;
// now referenced by Zig
worker->ref();
worker->lifecycleHandle_ = lifecycleHandle;
worker->m_workerCreationTime = MonotonicTime::now();

return worker;
Expand All @@ -228,7 +229,13 @@ Worker::~Worker()
Locker locker { allWorkersLock };
allWorkers().remove(m_clientIdentifier);
}
// m_contextProxy.workerObjectDestroyed();

if (lifecycleHandle_) {
auto* handle = lifecycleHandle_;
lifecycleHandle_ = nullptr;
WebWorkerLifecycleHandle__requestTermination(handle);
WebWorkerLifecycleHandle__release(handle);
}
}

ExceptionOr<void> Worker::postMessage(JSC::JSGlobalObject& state, JSC::JSValue messageValue, StructuredSerializeOptions&& options)
Expand Down Expand Up @@ -261,9 +268,14 @@ ExceptionOr<void> Worker::postMessage(JSC::JSGlobalObject& state, JSC::JSValue m

void Worker::terminate()
{
// m_contextProxy.terminateWorkerGlobalScope();
m_terminationFlags.fetch_or(TerminateRequestedFlag);
WebWorker__notifyNeedTermination(impl_);

if (lifecycleHandle_) {
auto* handle = lifecycleHandle_;
lifecycleHandle_ = nullptr;
WebWorkerLifecycleHandle__requestTermination(handle);
WebWorkerLifecycleHandle__release(handle);
}
}

// const char* Worker::activeDOMObjectName() const
Expand Down
3 changes: 2 additions & 1 deletion src/bun.js/bindings/webcore/Worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class WorkerGlobalScopeProxy;

struct StructuredSerializeOptions;
struct WorkerOptions;
struct WebWorkerLifecycleHandle;

class Worker final : public ThreadSafeRefCounted<Worker>, public EventTargetWithInlineData, private ContextDestructionObserver {
WTF_MAKE_TZONE_ALLOCATED(Worker);
Expand Down Expand Up @@ -119,7 +120,7 @@ class Worker final : public ThreadSafeRefCounted<Worker>, public EventTargetWith
// Tracks TerminateRequestedFlag and TerminatedFlag
std::atomic<uint8_t> m_terminationFlags { 0 };
const ScriptExecutionContextIdentifier m_clientIdentifier;
void* impl_ { nullptr };
WebWorkerLifecycleHandle* lifecycleHandle_ { nullptr };
};

JSValue createNodeWorkerThreadsBinding(Zig::GlobalObject* globalObject);
Expand Down
Loading
Loading