Treat SourceProviderSourceType::BunTranspiledModule as Module in debugger/inspector/cache/completion switches - #345
Conversation
…gger/inspector/cache/completion switches #73 added SourceProviderSourceType::BunTranspiledModule so Bun can attach a pre-computed module record to a SourceProvider and skip JSC's analyze-phase re-parse. Four sites that branch on the provider's sourceType() were left matching only ::Module, so a BunTranspiledModule provider falls through: - debugger/DebuggerParseData.cpp gatherDebuggerParseDataForSource: falls to the default:false arm, Debugger::resolveBreakpoint gets empty pause positions, and Debugger.setBreakpoint replies "Could not resolve breakpoint" for every line in the script. - inspector/agents/InspectorDebuggerAgent.cpp didParseSource: Debugger.scriptParsed reports module:false instead of true. - runtime/CachedTypes.cpp CachedSourceProvider encode/decode: would RELEASE_ASSERT_NOT_REACHED() if JSC bytecode caching encoded one of these providers. - runtime/Completion.cpp getSourceType: returns ScriptFetchParameters::Type::None instead of JavaScript. Currently reachable in Bun via `bun test --isolate` and `--compile` standalone executables; oven-sh/bun#35605 would route every runtime ESM provider through this type and is blocked on this. Each site adds a BUN_JSC_ADDITIONS-gated case that falls through to the Module arm.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughChangesBun transpiled module support
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
LGTM — mechanical additions of a USE(BUN_JSC_ADDITIONS)-gated BunTranspiledModule fallthrough to the four existing Module switch arms. The inline finding is pre-existing (unrelated to this diff) and noted for follow-up, not a blocker.
Extended reasoning...
Overview
Adds case SourceProviderSourceType::BunTranspiledModule: (or the equivalent || clause) at four sites that branch on SourceProvider::sourceType() — gatherDebuggerParseDataForSource, InspectorDebuggerAgent::didParseSource, CachedSourceProvider::encode/decode, and getSourceType in Completion.cpp. Each new case falls through to the existing Module handling and is gated behind #if USE(BUN_JSC_ADDITIONS), matching the enum member's own gating in SourceProvider.h.
Security risks
None. No new inputs, allocations, or trust boundaries — the change only widens existing enum matches by one Bun-specific value. Upstream (non-Bun) builds compile identically.
Level of scrutiny
Low. These are one-line case labels routing a Bun-only enum value to the same code path Module already takes. I confirmed via grep that these four sites plus the already-handled ModuleProgramExecutable.cpp / JSModuleLoader.cpp are the complete set of comparison sites on this enum, matching the PR description's claim. For CachedTypes.cpp, I verified that under USE(BUN_JSC_ADDITIONS) CachedStringSourceProvider::encode only reads base-class SourceProvider members (source().length() plus Base::encode), so the reinterpret_cast is no less safe for BunTranspiledModule than it already is for Module with a Zig::SourceProvider.
Other factors
The one inline finding (inverted VM comparison in clearInspectorBreakpointState) is explicitly flagged as pre-existing — it lives ~300 lines away from the diff, in symbolic-breakpoint cleanup code this PR does not touch, and is not a regression introduced here. It should be filed/fixed separately and does not affect approval of this change.
| String sourceMappingURL = sourceMapURLForScript(script); | ||
|
|
||
| m_frontendDispatcher->scriptParsed(scriptIDStr, script.url, script.startLine, script.startColumn, script.endLine, script.endColumn, script.isContentScript, sourceURL, sourceMappingURL, script.sourceProvider->sourceType() == JSC::SourceProviderSourceType::Module); | ||
| const auto providerSourceType = script.sourceProvider->sourceType(); |
There was a problem hiding this comment.
🟣 Pre-existing (not touched by this PR, noticed while reading the file): in clearInspectorBreakpointState() the replacedInternalFunctions().removeAllMatching lambda checks == &m_debugger.vm() where the parallel replacedThunks() block just above and both blocks in removeSymbolicBreakpoint() use !=. The inversion means this-VM entries are never removed on Debugger.disable (the InternalFunction debugger hooks stay installed and the Box<ReplacedInternalFunction> entries leak), and in a multi-VM process other-VM entries get their matchCount decremented by this agent's symbolic breakpoints. The actual line is ~2127; anchoring here because it's outside the diff.
Extended reasoning...
What the bug is
In InspectorDebuggerAgent::clearInspectorBreakpointState() (called from internalDisable() / willDestroyFrontendAndBackend()), the cleanup lambda for replacedInternalFunctions() has an inverted VM guard:
if (&replacedInternalFunction->internalFunction->vm() == &m_debugger.vm())
return false;Compare to the immediately preceding replacedThunks() block in the same function, and to both blocks in removeSymbolicBreakpoint(), which all use !=:
if (&replacedThunk->nativeExecutable->vm() != &m_debugger.vm())
return false;replacedInternalFunctions() is a process-global NeverDestroyed<Vector<Box<ReplacedInternalFunction>>> shared across all VMs/agents (guarded by s_replacedInternalFunctionsLock). The intent of the guard is: skip entries belonging to other VMs; only decrement/remove entries owned by this agent's VM. The == inverts that.
Step-by-step proof
Single-VM (typical Bun process):
- User connects an inspector and calls
Debugger.addSymbolicBreakpointwith e.g.symbol: "fetch". addSymbolicBreakpoint()walks the heap, finds matchingInternalFunctions, anddidCreateInternalFunction()swaps their native function pointers tointernalFunction{Call,Construct}WithDebuggerHookand appends aBox<ReplacedInternalFunction>to the global vector.- User disconnects →
willDestroyFrontendAndBackend()→internalDisable()→clearInspectorBreakpointState(). - The
removeAllMatchinglambda runs on each entry. For every entry,&entry->internalFunction->vm() == &m_debugger.vm()is true (there is only one VM), so the lambda returnsfalseand the entry is kept. ~ReplacedInternalFunction()never runs, so the InternalFunction's native function pointers are never restored — every subsequent call still routes throughinternalFunctionWithDebuggerHook, which takess_replacedInternalFunctionsLock, does a linearfind(), and callsvm.forEachDebugger(...)before dispatching. TheBoxentries also leak for the process lifetime.
Multi-VM process:
- VM A's agent adds a symbolic breakpoint on "foo"; VM B's agent adds one on "foo" too. The global vector has entries for both VMs.
- VM A's agent is disabled →
clearInspectorBreakpointState(). - For VM A's own entries,
== &m_debugger.vm()is true →return false→ kept (leak, as above). - For VM B's entries,
== &m_debugger.vm()is false → falls through to them_symbolicBreakpointsloop. VM A's "foo" breakpoint matches, so VM B's entry has itsmatchCountdecremented, potentially hitting 0 and being removed — restoring VM B's InternalFunction to its original pointer while VM B's debugger is still active and expects the hook.
Why nothing else prevents it
The only other place these entries are removed is removeSymbolicBreakpoint(), which is only called when the frontend explicitly issues Debugger.removeSymbolicBreakpoint. On disconnect / Debugger.disable, only clearInspectorBreakpointState() runs, and m_symbolicBreakpoints.clear() happens after the buggy block, so this is the sole opportunity to restore the hooks.
Fix
Flip == to != to match the three parallel sites:
if (&replacedInternalFunction->internalFunction->vm() != &m_debugger.vm())
return false;Relation to this PR
None. This PR only touches didParseSource() in this file to add a BunTranspiledModule case; it does not touch clearInspectorBreakpointState(), symbolic breakpoints, or the InternalFunction hook machinery. Flagged as pre-existing since we happened to be in this file.
Preview Builds
|
Problem
SourceProviderSourceType::BunTranspiledModule(added in #73) marks aSourceProviderthat carries Bun's pre-computed module record soJSModuleLoadercan build theJSModuleRecordwithout re-parsing. Four sites that branch onprovider->sourceType()only match::Module, so aBunTranspiledModuleprovider falls through:debugger/DebuggerParseData.cppgatherDebuggerParseDataForSourcefalse,Debugger::resolveBreakpointgets empty pause positions,Debugger.setBreakpointreplies"Could not resolve breakpoint"for every location in the script, andDebugger.setBreakpointByUrlsilently never resolvesinspector/agents/InspectorDebuggerAgent.cppdidParseSourceDebugger.scriptParsedsendsmodule: falseinstead oftrueruntime/CachedTypes.cppCachedSourceProvider::encode/decodeRELEASE_ASSERT_NOT_REACHED()if JSC's bytecode cache ever encodes one of these providersruntime/Completion.cppgetSourceTypeScriptFetchParameters::Type::Noneinstead ofJavaScriptReachable in Bun today via
bun test --isolateandbun build --compilestandalone executables:The same file without
--isolate(plain::Moduleprovider) reportsmodule: trueand the breakpoint resolves. oven-sh/bun#35605 would route every runtime ESM provider through this type and is blocked on this.Fix
Add a
USE(BUN_JSC_ADDITIONS)-gatedcase SourceProviderSourceType::BunTranspiledModule:at each site that falls through to the existingModulearm. These are the only four== ::Module/case ::Module:comparison sites on the enum; the remaining references construct providers of that type (jsc.cpp, JSScript.mm, WebCore) or already handle it (ModuleProgramExecutable.cpp,JSModuleLoader.cpp).CachedStringSourceProvider::encode/decodeunderUSE(BUN_JSC_ADDITIONS)only touch base-classSourceProvidermembers (source().length(),sourceOrigin(),sourceURL(), ...), so routing a non-StringSourceProvidersubclass through thereinterpret_castis as safe forBunTranspiledModuleas it already is forModule(Bun'sZig::SourceProvideris a directJSC::SourceProvidersubclass either way).The Bun-side regression test lands with the
WEBKIT_VERSIONbump.