webcore: revoke ObjectURLRegistry entries on Worker teardown#29908
webcore: revoke ObjectURLRegistry entries on Worker teardown#29908robobun wants to merge 2 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughThis PR implements automatic revocation of Blob/Object URLs created inside Workers upon Worker termination or natural exit. It adds context tracking to the ObjectURLRegistry so each registered URL is tagged with its originating execution context, then revokes all matching URLs during Worker shutdown. ChangesWorker-Created URL Auto-Revocation
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Review rate limit: 4/5 reviews remaining, refill in 12 minutes. Comment |
|
Updated 10:23 PM PT - May 3rd, 2026
@autofix-ci[bot], your commit 9e26a2e is building: |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/bun.js/VirtualMachine.zig`:
- Around line 1302-1307: createForTestIsolation() currently swaps the VM global
without cleaning up ObjectURL entries, causing blob URLs to leak because the old
global reuses the same context id; before replacing the VM's global in
createForTestIsolation(), call ObjectURLRegistry.revokeEntriesForContext() using
the old global's context id (obtainable via
VirtualMachine.scriptExecutionContextId(oldGlobal) or the old VM instance) to
revoke blob/object URLs tied to that context, mirroring the worker teardown path
that uses revokeEntriesForContext() in web_worker.zig.
In `@test/js/web/workers/worker_blob.test.ts`:
- Around line 129-193: The tests currently assert URL revocation only after
awaiting the worker "close" promise, which misses regressions where revocation
happens during the close event; update both tests to perform the assertion(s)
inside the worker's "close" event handler callback instead of after awaiting:
for the first test, move the loop that checks resolveObjectURL(url) returns
undefined into the close listener created with worker.addEventListener("close",
...) (the existing closed promise callback), and for the second test likewise
perform the expect(resolveObjectURL(url)).toBeUndefined() inside the close event
handler that currently resolves the promise; keep using the same variables
(worker, url(s), resolveObjectURL) and once: true listener semantics.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3b7218e9-6a00-4430-b125-6e5900850a2e
📒 Files selected for processing (4)
src/bun.js/VirtualMachine.zigsrc/bun.js/web_worker.zigsrc/bun.js/webcore/ObjectURLRegistry.zigtest/js/web/workers/worker_blob.test.ts
There was a problem hiding this comment.
I didn't find any bugs and the change follows existing patterns (same lock discipline as revoke(), same teardown placement as CronJob.clearAllForVM), but it touches the carefully-ordered worker exitAndDeinit path and CI is currently red on build-zig/build-cpp across all platforms, so this should get a human look before merging.
Extended reasoning...
Overview
This PR tags each ObjectURLRegistry entry with the creating VM's ScriptExecutionContext id, adds revokeEntriesForContext() to drop all entries for a given context under the existing registry lock, and calls it from WebWorker.exitAndDeinit() right after CronJob.clearAllForVM. A trivial VirtualMachine.scriptExecutionContextId() accessor is added, plus two regression tests in worker_blob.test.ts.
Security risks
None apparent. No new untrusted input is parsed; the new code path is a cleanup hook that iterates an internal map under an existing mutex and frees duped Blob entries. No auth, crypto, or permission surface is touched.
Level of scrutiny
Moderate-to-high. The diff is small and mechanically straightforward, but it inserts a new step into exitAndDeinit, which has a ~100-line comment block describing precise ordering invariants between JSC teardown, WebWorker__dispatchExit, arena destruction, and parent-thread observability. The new call runs on the worker thread, holds the process-global registry lock, and invokes Blob.deinit() (which derefs the underlying store) — this matches what the existing revoke() already does under the same lock, so it should be fine, but a maintainer familiar with Blob store lifetimes (file/S3-backed blobs, etc.) should confirm there's no teardown-ordering hazard.
Other factors
- CI (robobun, build #48818) reports build-zig and build-cpp failures on every platform for commit 5db3d1b; the subsequent autofix.ci commit is formatting-only, so it's unclear whether the build is green yet. I won't auto-approve past a red build.
- CodeRabbit's two minor notes (test-isolation cleanup and asserting inside the
closehandler) are scope-adjacent suggestions rather than defects in this change. - Good test coverage is added for both
terminate()and natural-exit paths, including a check that parent-context URLs survive.
There was a problem hiding this comment.
The core fix looks correct and well-tested, but this bundles a user-visible behavioral change (worker-created blob URLs are now auto-revoked) into the worker teardown path plus an unrelated-looking stripFlags build change in scripts/build/flags.ts — worth a human glance at both.
Extended reasoning...
Overview
This PR tags each ObjectURLRegistry.Entry with the creating VM's ScriptExecutionContext id, adds revokeEntriesForContext() (lock → collect matching UUIDs → fetchRemove + deinit), and calls it from WebWorker.exitAndDeinit() alongside the existing CronJob.clearAllForVM cleanup. A trivial VirtualMachine.scriptExecutionContextId() accessor is added, and two new tests in worker_blob.test.ts cover both terminate() and natural-exit paths, including a check that parent-context URLs are left alone. Separately, scripts/build/flags.ts narrows the Linux .eh_frame strip rule to c.lto only, to match the --no-eh-frame-hdr link flag and avoid a SIGSEGV in pthread_exit() unwind on non-LTO release builds.
Security risks
None identified. No new untrusted input parsing; the registry mutex is held for the new sweep exactly as it is for the existing revoke() path, and entry.deinit() under the lock matches the existing pattern. The build-flag change only narrows when sections are stripped (strictly more conservative).
Level of scrutiny
Medium-high. The Zig changes are small and mechanically sound (the two-pass collect-then-remove is the correct HashMap idiom, and blob.deinit() under the lock mirrors revoke()). However:
exitAndDeinit()is a historically delicate path (the file has ~100 lines of header docs on its threading model), and this adds a new cross-thread registry mutation there.- This is a user-visible behavioral change: code that previously relied on resolving a worker's blob URL after
terminate()will now getundefined. It matches the FileAPI spec and WebKit, but it's still an observable change worth a maintainer ack. - The
flags.tschange is build-infrastructure for the production strip step. The rationale in the comment is sound and the existing comment already notes CI release always has LTO on, so this should be a no-op for shipped binaries — but build-pipeline changes deserve a human look.
Other factors
The two CodeRabbit comments (test-isolation cleanup, assertion placement) were both resolved by the author with reasonable explanations and acknowledged as out-of-scope / intentional. No bugs were found by the bug-hunting system. Test coverage for the new behavior is good. CI shows Windows build-agent provisioning failures that appear to be infra, not code-related.
b3ac932 to
4c36073
Compare
There was a problem hiding this comment.
I didn't find any bugs, but this touches the worker teardown path (exitAndDeinit, threading-sensitive), introduces a user-visible behavior change for blob URL lifetime, and bundles an unrelated build-strip gating change in flags.ts — worth a quick human look.
Extended reasoning...
Overview
This PR fixes a memory leak where URL.createObjectURL() entries created inside a Worker were never removed from the process-global ObjectURLRegistry when the worker exited. It tags each registry Entry with the creating VM's ScriptExecutionContext id, adds revokeEntriesForContext() (lock-held two-pass collect-then-remove over the AutoHashMap), and calls it from WebWorker.exitAndDeinit() next to the existing CronJob.clearAllForVM cleanup. A trivial VirtualMachine.scriptExecutionContextId() accessor is added, and two regression tests are added to worker_blob.test.ts. Separately, scripts/build/flags.ts gates the .eh_frame strip step on c.lto so non-LTO linux-gnu release builds don't end up with a dangling PT_GNU_EH_FRAME phdr that crashes pthread_exit() during worker teardown.
I verified that the id used at registration time (vm.scriptExecutionContextId() → initial_script_execution_context_identifier) and at revocation time (this.execution_context_id) are the same value — initWorker at VirtualMachine.zig:1416 sets the former from the latter. The lock/iterate/remove pattern matches the existing revoke() and is correct for Zig's std.AutoHashMap (no removal during iteration). Entry.deinit() under the registry lock mirrors the existing single-revoke path, so any Blob-store deref thread-safety concern would be pre-existing.
Security risks
None identified. No auth, crypto, or untrusted-input parsing is touched. The registry mutation runs under its existing mutex; the new code path only deletes entries the dying worker itself created.
Level of scrutiny
Moderate-to-high. exitAndDeinit() is a threading-sensitive teardown path with a documented lock/ordering contract, and this is a user-visible behavior change (worker-created blob URLs now resolve to undefined after the worker exits — spec-correct and matches WebKit/Node, but still a change). The bundled flags.ts change alters how release binaries are stripped on linux-gnu; the rationale is sound and CI (always LTO) is unaffected, but it's a separate concern riding along in the same PR.
Other factors
The bug-hunting system found no issues. Both CodeRabbit inline comments were resolved by the author with reasonable explanations (test-isolation cleanup deferred as out of scope; assertion placement after await closed is equivalent and avoids a hang-on-failure mode). No CODEOWNERS apply. The new tests cover both terminate() and natural exit and assert parent-context URLs survive. Overall the implementation looks correct to me, but the combination of worker-lifecycle code, behavior change, and build-config tweak makes this worth a human reviewer's eyes rather than a bot approval.
4c36073 to
dfcc6fe
Compare
There was a problem hiding this comment.
I didn't find any bugs, but this combines a user-visible behavioral change (worker-created blob URLs now auto-revoke on teardown) with a build-system change to Linux strip flags and touches the cross-thread worker teardown path — worth a human look.
Extended reasoning...
Overview
This PR tags each ObjectURLRegistry entry with the registering VM's ScriptExecutionContext id, adds revokeEntriesForContext() (lock-held collect-then-remove sweep), and calls it from WebWorker.exitAndDeinit() so blob URLs created inside a Worker are dropped when the Worker exits. It also adds a trivial scriptExecutionContextId() accessor on VirtualMachine, two regression tests in worker_blob.test.ts, and — separately — gates the Linux glibc .eh_frame/.eh_frame_hdr strip step on c.lto in scripts/build/flags.ts so non-LTO release builds don't SIGSEGV in pthread_exit() unwind during Worker teardown.
Security risks
None identified. No auth/crypto/permissions surface; the registry sweep runs under the existing mutex, and the only new cross-thread interaction is Blob.deinit() on the worker thread for a duped Blob whose Store already uses atomic refcounting.
Level of scrutiny
Medium-high. Individually each piece is small and well-reasoned, but together they touch:
- Worker teardown (
exitAndDeinit), which the file's own header documents as a delicate cross-thread sequence with strict ordering invariants. The new call is placed aftercloseAllSocketGroupsand beforeWebWorker__dispatchExit, readingthis.execution_context_idwhilethisis still guaranteed live — that placement looks correct, but this path historically rewards careful eyes. - A user-observable behavior change: blob URLs created in a Worker are no longer resolvable from the parent after the Worker exits. Spec-aligned and matches WebKit's
unregisterURLsForContext, but still a semantic change someone could be relying on. - A release-binary build change: narrowing when
.eh_frame*sections are stripped on Linux glibc. The rationale (matching the existing--no-eh-frame-hdrLTO gate soPT_GNU_EH_FRAMEnever dangles) is sound and well-commented, but build-flag changes that affect what ships deserve a maintainer sign-off.
Other factors
The bug-hunter found no issues. Both CodeRabbit threads were reasonably resolved by the author (test-isolation gap is pre-existing/out-of-scope; assertion placement is correct). Tests cover both terminate() and natural exit, and verify parent-context URLs are untouched. The revokeEntriesForContext implementation correctly avoids mutate-during-iterate and deinit()s each removed entry. I'm deferring rather than approving primarily because of the bundled build-flag change and the behavioral-change nature of the fix, not because of any specific concern with the code.
dfcc6fe to
07d887e
Compare
There was a problem hiding this comment.
I didn't find any bugs, but this combines a user-observable behavior change in worker teardown (blob URLs now auto-revoked) with a build-time strip-flag fix in scripts/build/flags.ts — worth a human look at both the concurrency placement in shutdown() and the .eh_frame strip gating.
Extended reasoning...
Overview
This PR fixes a Blob leak by tagging each ObjectURLRegistry entry with the creating VM's ScriptExecutionContext id and sweeping matching entries during WebWorker.shutdown(). It touches src/bun.js/VirtualMachine.zig (trivial accessor), src/bun.js/webcore/ObjectURLRegistry.zig (new context_id field + revokeEntriesForContext under the registry lock), src/bun.js/web_worker.zig (one call in the teardown sequence), and adds two regression tests in worker_blob.test.ts. It also includes a separate build-system change in scripts/build/flags.ts that gates .eh_frame section stripping on c.lto to avoid a SIGSEGV in pthread_exit() unwind on non-LTO GNU/Linux builds.
Security risks
None identified. The registry remains process-global and mutex-guarded; the new sweep runs under the same lock as existing revoke() and calls the same Entry.deinit() path. No new user-controlled inputs, no auth/crypto/permissions surface.
Level of scrutiny
Moderate-to-high. The Zig change is small and follows the existing CronJob.clearAllForVM / closeAllSocketGroups pattern in the same block, and Blob.deinit() under the registry lock matches the existing single-entry revoke(). However: (1) this is a user-observable behavior change — code that previously relied on worker-created blob URLs surviving terminate() will now see them gone, which is spec-correct but worth a maintainer ack; (2) the call sits in the worker-thread shutdown sequence between closeAllSocketGroups and WebWorker__dispatchExit, a concurrency-sensitive region; (3) the flags.ts change is logically independent of the registry fix and affects how release binaries are stripped on linux-gnu — it looks correct (matches the --no-eh-frame-hdr gating in linkerFlags) but a build-system reviewer should confirm.
Other factors
Two new tests cover both terminate() and natural-exit paths and verify parent-context URLs are unaffected. CodeRabbit raised two minor points (test-isolation sweep, assertion placement) that the author resolved with sound reasoning, and both threads are marked resolved. CI build #49773 is in progress. The combination of a behavior change in a critical teardown path plus an orthogonal build-flag tweak puts this outside the "simple/mechanical" bar for auto-approval.
07d887e to
9e26a2e
Compare
URL.createObjectURL() stores a duped Blob in a process-global registry. When called from a Worker, those entries were never removed if the worker terminated without calling URL.revokeObjectURL(), leaking the Blob for the lifetime of the process. Tag each registry entry with the ScriptExecutionContext id of the VM that created it, and sweep entries for that id from WebWorker.exitAndDeinit (alongside the existing per-VM CronJob cleanup). This matches the FileAPI spec's unloading-cleanup step and WebKit's BlobURLRegistry::unregisterURLsForContext.
9e26a2e to
013bbc1
Compare
There was a problem hiding this comment.
LGTM — focused leak fix that mirrors the adjacent CronJob.clearAllForVM teardown pattern, with tests covering both terminate and natural exit.
Extended reasoning...
Overview
This PR fixes a process-lifetime memory leak where URL.createObjectURL() calls inside a Worker left duped Blob entries in the process-global ObjectURLRegistry after the worker exited. It adds a context_id: u32 tag to each registry entry, a scriptExecutionContextId() accessor on VirtualMachine, and a revokeEntriesForContext() sweep that is invoked from WebWorker.shutdown() right alongside the existing CronJob.clearAllForVM cleanup. Two new tests in worker_blob.test.ts cover terminate() and natural exit, and explicitly verify that parent-context URLs are left intact.
Security risks
None. No new inputs are parsed; the change only adds a per-context sweep of an existing in-process map under its existing mutex. There is no auth, crypto, or untrusted-data handling involved.
Level of scrutiny
Worker teardown is concurrency-sensitive, but the addition is a single mutex-protected map sweep placed in the same shutdown phase as CronJob.clearAllForVM (after vm.onExit(), before WebWorker__dispatchExit). revokeEntriesForContext holds the registry lock for the entire collect-then-remove pass — the same lock discipline as the pre-existing revoke(), which already calls entry.deinit() under the lock from arbitrary threads. The two-pass (collect keys, then fetchRemove) avoids mutating the HashMap during iteration. The @intCast to u32 is safe since context IDs are always positive (main = 1, workers from generateIdentifier()).
Other factors
The bug hunter found no issues. Both CodeRabbit inline comments are resolved: the bun test --isolate gap is pre-existing and out of scope (this PR makes a future fix possible by adding the context_id tag), and the test-assertion-placement suggestion was reasonably declined (asserting after await closed is equivalent given the revoke happens before dispatchExit, and avoids hang-on-failure). No CODEOWNERS cover these files. The fix mirrors WebKit's BlobURLRegistry::unregisterURLsForContext and the FileAPI spec's unloading-cleanup step.
What
URL.createObjectURL(blob)stores a dupedBlobin a process-globalObjectURLRegistry. When called inside a Worker, those entries were never removed if the worker exited (naturally or viaterminate()) without an explicitURL.revokeObjectURL(), so the Blob data leaked for the lifetime of the process.Repro
Cause
ObjectURLRegistryis a singletonUUID → Entrymap with no notion of which context created the entry.WebWorker.exitAndDeinit()tears down the worker's VM but never touches the registry, so every worker-created entry is orphaned.Fix
Entrywith the creating VM'sScriptExecutionContextid (via a newVirtualMachine.scriptExecutionContextId()helper).ObjectURLRegistry.revokeEntriesForContext(context_id)which drops all matching entries under the registry lock.WebWorker.exitAndDeinit()alongside the existingCronJob.clearAllForVMcleanup, beforeWebWorker__dispatchExitso the parent'scloseevent observes the URLs as already revoked.This mirrors WebKit's
BlobURLRegistry::unregisterURLsForContext/PublicURLManager::stop()and the FileAPI spec's unloading-cleanup step for blob URL stores.Verification
New tests in
test/js/web/workers/worker_blob.test.ts:blob URLs created inside a Worker are revoked when the worker terminates— creates several blob URLs in a worker, confirms they resolve while alive, then asserts they're gone afterterminate(). Also asserts a parent-context URL is left alone.blob URLs created inside a Worker are revoked when the worker exits naturally— same for natural worker exit.Both fail on
main(Received: Blob (N bytes)whereundefinedis expected) and pass with this change.