microsoft / microsoft/vscode-js-debug
Sourcemap resolution fails when multiple V8 execution contexts load the same script URL (race condition in addSource)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 2k
- Forks
- 373
- Avg merge
- 1d 9m
- Merged PRs (30d)
- 6
Description
Environment
- js-debug version: nightly 2026.4.317 (also checked latest
mainbranch source — same code) - VS Code version: latest
- Debug type:
pwa-node(attach mode, CDP) - OS: Windows 11
Description
When debugging an application that creates multiple V8 execution contexts (e.g., a Chromium Embedded Framework app with ~50 contexts), source map resolution silently fails for scripts that are loaded in more than one context with the same URL.
The debugger shows the compiled source instead of the original TypeScript source, and breakpoints set in .ts files are not hit.
Root Cause Analysis
The issue is a race condition in SourceContainer.addSource() (src/adapter/sourceContainer.ts).
Timeline
-
scriptParsedevent #1 arrives forshared.js(scriptId 224, executionContextId 47).
→_onScriptParsedchecksgetSourceByOriginalUrl("shared.js")→undefined→ proceeds to calladdSource(). -
scriptParsedevent #2 arrives forshared.js(scriptId 241, executionContextId 48).
→_onScriptParsedchecksgetSourceByOriginalUrl("shared.js")→ stillundefined(Source1 not registered yet) → also proceeds to calladdSource(). -
Inside
addSource(),await this.sourcePathResolver.urlToAbsolutePath(...)yields execution. Both calls are now in-flight concurrently. -
Call #1 resumes: creates
Source1, calls_addSource(Source1)→ registers Source1 in_sourceByOriginalUrland_sourceByReference. Starts async_finishAddSourceWithSourceMap(Source1). -
Call #2 resumes: creates
Source2, calls_addSource(Source2).
→ In_addSource, the existing-URL check finds Source1 → removes Source1 (callsremoveSource) → registers Source2. -
_finishAddSourceWithSourceMap(Source1)resumes after loading the.mapfile. It checks:if (this._sourceByReference.get(source.sourceReference) !== source) { return deferred.resolve(undefined); // ← Source1 was evicted! }Source1 has been evicted → sourceMap resolves to
undefined. -
Result: scriptId 224 is permanently linked to Source1 (which has no sourceMap). When the debugger pauses in scriptId 224,
_originalPositionFor()returnsUnmappedReason.HasNoMap→ no source mapping occurs.
Evidence from DAP trace log
- Two
loadedSourceevents for the same file, both with"reason": "new"(dedup failed):{"seq":0,"type":"event","event":"loadedSource","body":{"reason":"new","source":{"name":"shared.js","path":"...\\shared.js","sourceReference":1936498801}}} {"seq":0,"type":"event","event":"loadedSource","body":{"reason":"new","source":{"name":"shared.js","path":"...\\shared.js","sourceReference":1936498801}}} stackTraceresponse shows compiled positions only — no sourcemap resolution despite a valid.mapfile being loaded.
Existing TODO in code
There's already a TODO comment in _addSource acknowledging this scenario:
// todo: we should allow the same source at multiple uri's if their scripts
// have different executionContextId. We only really need the overwrite
// behavior in Node for tools that transpile sources inline.
Suggested Fix
Add a deduplication check in addSource() after the await, before creating a new Source:
public async addSource(
event: Cdp.Debugger.ScriptParsedEvent,
contentGetter: ContentGetter,
sourceMapUrl?: string,
inlineSourceRange?: InlineScriptOffset,
runtimeScriptOffset?: InlineScriptOffset,
contentHash?: string,
): Promise<Source> {
const absolutePath = await this.sourcePathResolver.urlToAbsolutePath({ url: event.url });
// --- FIX: reuse existing source if same URL + same content ---
if (contentHash) {
const existing = this._sourceByOriginalUrl.get(event.url);
if (existing && existing.contentHash === contentHash) {
return existing;
}
}
// --- END FIX ---
// ... rest of the method
This ensures that when two scriptParsed events for the same script (same URL, same content hash) race through addSource(), the second call reuses the already-registered Source instead of creating a duplicate that evicts the first.
Reproduction
Any application that:
- Creates multiple V8 execution contexts (e.g., web workers, iframes, or embedded V8 environments)
- Loads the same script URL in multiple contexts
- Uses source maps
Will trigger this bug. The more contexts that load the same script, the higher the probability of hitting the race window.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/adapter/sourceContainer.ts, especially addSource(), _addSource(), and _finishAddSourceWithSourceMap(). Trace the concurrent scriptParsed flow around the sourcePathResolver await, then reproduce with multiple V8 contexts loading the same URL and a source map. Done means duplicate loading no longer evicts the source whose map is still being resolved, and original TypeScript positions remain available.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100