Skip to content

Fix Worker preload resolve error reporting 'undefined' - #32369

Closed
robobun wants to merge 6 commits into
mainfrom
farm/c3d3c63b/worker-preload-resolve-error
Closed

robobun wants to merge 6 commits into
mainfrom
farm/c3d3c63b/worker-preload-resolve-error

Conversation

@robobun

@robobun robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes new Worker(entry, { preload: ["./missing.js"] }) throwing TypeError: undefined instead of the actual resolve error.

Repro:

bun -e 'try { new Worker("./entry.js", { preload: ["./does-not-exist.js"] }); } catch (e) { console.log(e.constructor.name + ":", JSON.stringify(e.message)); }'

Before: TypeError: "undefined"
After: TypeError: "BuildMessage: ModuleNotFound resolving \"./does-not-exist.js\" (entry point)"

Cause: two compounding issues in WebWorker::create / resolve_entry_point_specifier (src/jsc/web_worker.rs):

  1. set_log(&raw mut temp_log) stored the stack address of temp_log in the transpiler, and temp_log was then moved into a scopeguard tuple. The transpiler's raw *mut Log pointed at moved-from stack.
  2. resolve_entry_point_specifier took log: &mut Log as a parameter that aliased (*parent).transpiler.log. resolve_entry_point writes the error through transpiler.log; under release optimization the &mut parameter's noalias lets the compiler assume log.msgs is still empty when log.to_js() reads it, so it returns JSValue::UNDEFINED (LogJsc::to_js, count == 0) and to_bun_string() yields "undefined".

The Zig reference (web_worker.zig:288) avoids both: &temp_log with no move, and the log is passed as a raw *Log (no noalias).

Fix:

  • Move temp_log into the guard first, then call set_log with its post-move address.
  • Drop the log parameter from resolve_entry_point_specifier; read (*parent).transpiler.log directly after the resolve so the read and write share one provenance path.
  • Move spin()'s vm_log binding into the None arm so it is not held as &mut across the resolve call (same aliasing class).

How did you verify your code works?

New test test/js/web/workers/worker-preload-resolve-error.test.ts: constructs a Worker with a nonexistent preload module and asserts the caught error message is not "undefined" and contains the module specifier.

  • bun bd test and bun run build:release test with main's src/: fails (message is "undefined").
  • bun bd test and bun run build:release test with the fix: passes.
  • Full worker.test.ts on release: 23 pass, 1 pre-existing todo.

The test lives in its own file because worker.test.ts has two pre-existing 1000ms-timeout tests ("worker with event listeners doesn't close event loop") that flake on slow debug+ASAN runners independently of this change (also noted in #31951).

WebWorker::create stored the stack address of temp_log in the
transpiler's log pointer, then moved temp_log into a scopeguard
tuple. The transpiler wrote the ModuleNotFound message through the
stale pointer; the guard's temp_log stayed empty, so log.to_js()
returned undefined and the thrown TypeError's message was the
string 'undefined'.

Move temp_log into the guard first, then call set_log with its
final address.
@robobun

robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:48 PM PT - Jun 15th, 2026

✅ @robobun, your commit ef918a1773552aca151d6c704ca3a29dcaee4f56 passed in Build #62749! 🎉


🧪   To try this PR locally:

bunx bun-pr 32369

That installs a local version of the PR into your bun-32369 executable, so you can run:

bun-32369 --bun

@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 844e86a2-3938-414a-94ef-2b17aba8b354

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa9ddd and ef918a1.

📒 Files selected for processing (1)
  • test/no-validate-leaksan.txt

Walkthrough

resolve_entry_point_specifier loses its explicit &mut bun_ast::Log parameter and now reads the log from (*parent).transpiler.log on failure. WebWorker__create moves temp_log into the scopeguard tuple before calling transpiler.set_log, keeping the raw pointer valid on all exit paths. Both call sites drop the log argument; spin() defers vm.log_mut() to the failure branch only. A new test asserts that an unresolvable preload module produces a meaningful error message.

Changes

WebWorker preload error propagation fix

Layer / File(s) Summary
resolve_entry_point_specifier: remove log param, read from transpiler
src/jsc/web_worker.rs
Drops the log: &mut bun_ast::Log parameter from the function signature; on resolve_entry_point failure, loads log from (*parent).transpiler.log via the raw pointer and uses it for the to_js conversion.
WebWorker__create RAII fix and updated call sites
src/jsc/web_worker.rs
Scopeguard now owns (parent_ref, temp_log) so the transpiler's raw *mut Log pointer targets stable guard-owned storage on all return paths. Both the WebWorker__create and spin() invocations of resolve_entry_point_specifier drop the log argument; spin() defers vm.log_mut() to the None branch only.
Test: unresolvable preload error message and CI config
test/js/web/workers/worker-preload-resolve-error.test.ts, test/no-validate-leaksan.txt
New test instantiates a Worker with a missing preload specifier, catches the thrown Error, and asserts the message is defined (not "undefined") and contains the missing module path. Test is excluded from LeakSanitizer validation in ASAN CI.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing an issue where Worker preload resolution errors were incorrectly reported as 'undefined'.
Description check ✅ Passed The description covers both required template sections with comprehensive detail: it explains what the PR does with a concrete reproduction case and before/after comparison, and thoroughly documents verification through a new test.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — correct reordering so set_log targets the post-move address inside the scopeguard, with a regression test.

Extended reasoning...

Overview

This PR fixes a dangling-pointer bug in WebWorker::create (src/jsc/web_worker.rs) where set_log(&raw mut temp_log) was called before temp_log was moved into a scopeguard tuple, leaving the transpiler holding a stale stack address. The fix reorders the operations: construct the guard first (moving the Log in), then call set_log on the address obtained via &mut *restore. A regression test in worker.test.ts asserts the error message for an unresolvable preload module is not "undefined" and contains the specifier.

Security risks

None. This only affects the content of an error message on the Worker-construction failure path. No auth, crypto, permissions, or untrusted-input parsing is involved.

Level of scrutiny

Low-to-medium. The src change is ~5 lines of reordering with a clarifying comment, confined to an error-reporting path. The root cause (raw *mut Log stored in the transpiler, then the pointee moved) is well-explained and the fix is the obvious correct one — restore is a stack local that is never moved after the set_log call, so the address obtained through DerefMut stays valid for the guard's lifetime. The closure still restores prev_log and drops the temp log on every return path, exactly as before.

Other factors

  • The PR description includes a clear repro, root-cause analysis (including why log.to_js() on an empty log yields undefined), and verification that the new test fails without the fix and passes with it.
  • The test is added under the existing preload describe block and follows the surrounding test patterns.
  • No outstanding reviewer comments; CI build was triggered.

The previous commit fixed the address (temp_log moved into the guard
before set_log), which worked on debug builds. On release, the
separate &mut Log parameter to resolve_entry_point_specifier carries
noalias, so the optimizer could not see resolve_entry_point's write
through transpiler.log into the same allocation, and to_js() still
read an empty log.

Drop the log parameter; read (*parent).transpiler.log directly after
the resolve call so the read and write share one provenance path.
Move spin()'s vm_log binding into the None arm so it is not held as
&mut across the call either.

Move the regression test to its own file to avoid pre-existing
1000ms-timeout flakes in worker.test.ts on slow ASAN runners.
Comment thread test/js/web/workers/worker-preload-resolve-error.test.ts
robobun added 2 commits June 16, 2026 00:50
The in-process new Worker() call on the leaksan lane reported the
BuildMessage's external-string backing buffer as leaked (it is
JS-heap-owned and freed by GC, which does not run before process
exit). worker.test.ts is already on the no-validate-leaksan list for
the same class of report; spawning a child with bunEnv avoids leak
detection without adding another exclusion.
The error path creates a BuildMessage whose to_string_fn hands an
external-string backing buffer to JSC; it is freed at GC, which does
not run before the subprocess exits. worker.test.ts, worker_blob.test.ts
and message-channel.test.ts are already on this list for the same
class of JS-heap-lifetime report.
Comment thread test/js/web/workers/worker-preload-resolve-error.test.ts Outdated
If the child aborts, stdout is empty and JSON.parse throws an
opaque EOF error. Assert the combined { stdout, stderr, exitCode }
object first so the CI failure diff shows the actual diagnostic.
Comment thread test/no-validate-leaksan.txt
The subprocess wrapper was added to avoid adding a leaksan exclusion,
but bunEnv spreads process.env so the child inherited detect_leaks=1
anyway. With the file now on no-validate-leaksan.txt (same class as
worker.test.ts), an in-process try/catch has identical LSan behavior
and drops ~25 lines of scaffolding.
@robobun

robobun commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: this branch conflicts with main and was never rebased. #41496 fixes the same root cause on current main, with a preload specific message.

@robobun robobun closed this Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant