Treat SourceProviderSourceType::BunTranspiledModule as Module in debugger/cache/completion switches - #345
Conversation
WalkthroughThe changes add conditional ChangesBun 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
|
…gger/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. Three sites that branch on the provider's sourceType() still match 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. - 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. (InspectorDebuggerAgent::didParseSource, the fourth site in the original version of this change, is covered since the upstream merge in #383 by scriptTypeForScript.) Currently reachable in Bun via `bun test --isolate` and `--compile` standalone executables; oven-sh/bun#35605 routes 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.
64b7e43 to
b7c6939
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
Rebased onto current main (447082a) at b7c6939. The oven-sh/bun#35605 (which turns |
There was a problem hiding this comment.
LGTM — mechanical addition of USE(BUN_JSC_ADDITIONS)-gated BunTranspiledModule case labels that fall through to the existing Module arms.
What was reviewed:
- Confirmed the three switch sites are the only remaining
::Module-only comparisons on main;scriptTypeForScriptalready handles it, so the dropped inspector hunk is correct. - Checked
CachedStringSourceProvider::encode/decodeunderBUN_JSC_ADDITIONS— only base-classSourceProvidermembers are touched, so thereinterpret_castis no less safe forBunTranspiledModulethan it already is forModule. - The pre-existing
clearInspectorBreakpointStateissue noted earlier is unrelated to this diff and no longer in a touched file.
Extended reasoning...
Overview
This PR adds a case SourceProviderSourceType::BunTranspiledModule: label (guarded by #if USE(BUN_JSC_ADDITIONS)) at three switch sites so the case falls through to the existing SourceProviderSourceType::Module handling:
debugger/DebuggerParseData.cpp—gatherDebuggerParseDataForSourcenow parses these providers as modules instead of returningfalse.runtime/CachedTypes.cpp—CachedSourceProvider::encode/decoderoute throughCachedStringSourceProviderinstead of hittingRELEASE_ASSERT_NOT_REACHED().runtime/Completion.cpp—getSourceTypereturnsScriptFetchParameters::Type::JavaScriptinstead ofNone.
After the rebase onto 447082a, the original fourth hunk in InspectorDebuggerAgent.cpp was dropped because scriptTypeForScript on main already maps BunTranspiledModule to ScriptType::Module — verified in the current tree.
Security risks
None. No new inputs, no parsing changes, no auth/crypto/permissions surface. The only effect is that an existing Bun-specific enum value takes the same code path as Module instead of the default/assert fallthrough. All additions are behind USE(BUN_JSC_ADDITIONS), so upstream/non-Bun builds are byte-identical.
Level of scrutiny
Low. This is a 12-line diff consisting purely of additional case labels with no new logic. The one non-trivial concern is the reinterpret_cast<const StringSourceProvider&> in CachedSourceProvider::encode: I checked CachedStringSourceProvider::encode and CachedSourceProviderShape::encode under USE(BUN_JSC_ADDITIONS) — they only invoke base-class SourceProvider methods (sourceOrigin(), sourceURL(), source().length(), etc.), so passing a non-StringSourceProvider here is exactly as safe as it already is for Module providers (Bun's Zig::SourceProvider is a direct SourceProvider subclass in either case). decode under Bun returns SourceProvider* and prefers the runtime-supplied provider, so no new subclass assumptions are introduced there either.
Other factors
- A grep for
SourceProviderSourceType::Module\bconfirms the PR description: the remaining references are constructors (jsc.cpp,JSScript.mm) or sites that already handleBunTranspiledModule(ModuleProgramExecutable.cpp,InspectorDebuggerAgent.cpp). - The bug hunter found no issues in this diff. My earlier inline comment on this PR flagged a pre-existing inverted VM check in
InspectorDebuggerAgent::clearInspectorBreakpointState; that file is no longer touched by the rebased diff and the issue is orthogonal to this change, so it does not block approval. - Regression coverage lands on the Bun side with the
WEBKIT_VERSIONbump per the description.
With ModuleInfo attached, every runtime ES module reaches JSC as a BunTranspiledModule SourceProvider. JSC's DebuggerParseData only gathers pause positions for Program/Module, so Debugger.setBreakpoint replies 'Could not resolve breakpoint' and setBreakpointByUrl resolves nothing until the WebKit side (oven-sh/WebKit#345) lands. This pins the expected behaviour; it fails on this branch until WEBKIT_VERSION is bumped past that change and passes on main today (plain Module providers).
autobuild-preview-pr-345-b7c69395 adds the BunTranspiledModule arms to DebuggerParseData, CachedTypes and Completion on top of 447082ab, which is what lets inspect-module-breakpoints.test.ts pass now that every runtime ES module is a BunTranspiledModule provider. To be repinned to the merge sha's autobuild before this lands.
|
Superseded by #405 (this change rebased, plus the |
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.