-
Notifications
You must be signed in to change notification settings - Fork 4.4k
ai slop #29180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ai slop #29180
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -262,7 +262,8 @@ ExceptionOr<void> Worker::postMessage(JSC::JSGlobalObject& state, JSC::JSValue m | |
| void Worker::terminate() | ||
| { | ||
| // m_contextProxy.terminateWorkerGlobalScope(); | ||
| m_terminationFlags.fetch_or(TerminateRequestedFlag); | ||
| if (m_terminationFlags.fetch_or(TerminateRequestedFlag) & (TerminateRequestedFlag | TerminatedFlag)) | ||
| return; | ||
| WebWorker__notifyNeedTermination(impl_); | ||
| } | ||
|
Comment on lines
262
to
268
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The PR's guard on terminate() is not applied to setKeepAlive(): calling worker.ref() or worker.unref() after a worker exits naturally but before the TerminatedFlag task runs will pass a freed impl_ pointer to WebWorker__setRef(). This is the same UAF race as the one fixed in terminate(), just via a different code path (Worker.cpp:134-137). Extended reasoning...What the bug is Worker::setKeepAlive() (Worker.cpp:134-137) calls WebWorker__setRef(impl_, keepAlive) unconditionally, with no guard against impl_ having been freed. The PR correctly added a guard to terminate(), but setKeepAlive() was overlooked. The race window setKeepAlive() is reached from worker.ref() and worker.unref() (JSWorker.cpp:636, 647), which can be called by user JavaScript at any time. The vulnerable sequence is:
Why existing code does not prevent it setKeepAlive() has zero guards — it unconditionally forwards to Zig. In Zig, setRef() (web_worker.zig:580-586) begins with "if (this.hasRequestedTerminate())", which reads this.requested_terminate from the freed struct. This is a use-after-free / use-after-poison. Unlike terminate(), which now checks m_terminationFlags on the safe C++ object before touching impl_, setKeepAlive() performs no such check. Step-by-step proof const w = new Worker("does-not-exist"); Under ASAN this will reliably trigger use-after-poison. How to fix Apply the same guard used in terminate(): void Worker::setKeepAlive(bool keepAlive) |
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 The fix correctly prevents double-terminate UAF (TerminateRequestedFlag path), but three related UAF windows remain open: (1) a timing race where a naturally-exiting worker frees its Zig struct before the async dispatchExit task sets TerminatedFlag, leaving terminate() unguarded; (2) a deterministic UAF when terminate() is called from inside a close event handler (TerminatedFlag is set only after dispatchCloseEvent returns); and (3) a TOCTOU in notifyNeedTermination where the struct can be freed between the status check and the subsequent setRequestedTerminate() call. All three involve WebWorker__notifyNeedTermination(impl_) being called on freed memory and are not covered by the new guard.
Extended reasoning...
Root of the remaining bugs
The PR adds a guard in
Worker::terminate()(Worker.cpp:262–268): iffetch_or(TerminateRequestedFlag)reveals that eitherTerminateRequestedFlagorTerminatedFlagwas already set, the function returns early without touchingimpl_. This correctly closes the double-terminate window. However,TerminatedFlagis set asynchronously by a task posted indispatchExit(), while the Zig struct is freed synchronously inexitAndDeinit()immediately after the post. This mismatch creates three remaining UAF windows.Bug 1 (timing race before the dispatchExit task runs — Worker.cpp:266)
In
exitAndDeinit()(web_worker.zig ~lines 626–662):this.setStatus(.terminated)— Zig-side status updatedWebWorker__dispatchExit(...)— posts an async task to the main thread; that task will eventually setm_terminationFlags |= TerminatedFlagthis.deinit()— immediately callsbun.default_allocator.destroy(this), freeingimpl_Between step 3 and when the posted task actually runs on the main thread,
impl_is a dangling pointer whilem_terminationFlagsstill has neitherTerminateRequestedFlagnorTerminatedFlagset. If JS callsworker.terminate()in this window, the PR's guard evaluates to0 & (TerminateRequestedFlag | TerminatedFlag) == 0and falls through, passing the freed pointer toWebWorker__notifyNeedTermination. Readingthis.statusinsidenotifyNeedTerminationis a use-after-free.Concrete proof: create a worker pointing at a non-existent URL; the Zig start() function immediately observes the error, calls exitAndDeinit(), and frees the struct — potentially before the main thread's event loop processes the async TerminatedFlag task. Running
w.terminate()immediately after with a millisecond delay (but not a full event-loop turn) reliably triggers the race under ASAN.Bug 2 (deterministic UAF from a close event handler — Worker.cpp:445–447)
In the async task that
dispatchExit()posts, the main thread executes:When a user's close handler calls
w.terminate():TerminateRequestedFlag= 0 (terminate() was never called before)TerminatedFlag= 0 (set only after this handler returns)The guard is not triggered.
WebWorker__notifyNeedTermination(impl_)is called, butimpl_was already freed when the worker thread calledthis.deinit()right after posting the task. This is 100% deterministically reproducible with:const w = new Worker('bad-url'); w.addEventListener('close', () => w.terminate());.One verifier argued this is a duplicate of Bug 1 because they share the same root cause (TerminatedFlag set asynchronously). The scenarios are however distinct trigger paths: Bug 1 fires before the async task runs at all; Bug 2 fires from inside the task during the close handler. The fix for Bug 1 (set TerminatedFlag synchronously on the worker thread before deinit) happens to also fix Bug 2, but the deterministic reproducibility of Bug 2 makes it the most immediately dangerous manifestation.
Bug 3 (TOCTOU inside notifyNeedTermination — web_worker.zig ~lines 603–619)
Even if the C++ guard in terminate() passes correctly (i.e.,
impl_is valid at call entry), there is a second race window withinnotifyNeedTermination:Between the status check (which sees a non-terminated status, so proceeds) and the subsequent
setRequestedTerminate(), the worker thread can completesetStatus(.terminated)+WebWorker__dispatchExit()+this.deinit()in full, freeing the struct.setRequestedTerminate()then writes to freed memory. The window is narrow (two adjacent atomic operations) but valid under thread preemption or TSAN stress testing.How to fix
The fundamental issue is that
impl_is freed beforeTerminatedFlagis set. A safe approach is to setTerminatedFlagon the C++Workerobject synchronously on the worker thread before callingdeinit(), e.g. via a new exported functionWebWorker__markTerminated(cpp_worker)that doesworker->m_terminationFlags.fetch_or(TerminatedFlag). Alternatively, null out and guardimpl_under a mutex before freeing. Additionally,notifyNeedTerminationneeds a mutex or generation counter to guard the TOCTOU between its status check and subsequent field accesses.