Skip to content

Fix nil pointer dereference in aliasResolver.GetSourceFile#2814

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/fix-nil-pointer-dereference
Draft

Fix nil pointer dereference in aliasResolver.GetSourceFile#2814
Copilot wants to merge 3 commits intomainfrom
copilot/fix-nil-pointer-dereference

Conversation

Copy link
Contributor

Copilot AI commented Feb 18, 2026

aliasResolver.GetSourceFile passes the result of host.GetSourceFile() directly to binder.BindSourceFile() without a nil check. Unlike compiler.Program.GetSourceFile (which does a map lookup over pre-loaded files), the alias resolver loads files on-demand from SnapshotFS, which returns nil when a file doesn't exist. This crashes the language server during auto-import resolution.

The checker already handles nil from GetSourceFileForResolvedModule (checker.go:14809), so returning nil is correct behavior.

  • aliasresolver.go: Guard against nil before calling binder.BindSourceFile
  • registry.go: Guard against nil GetSourceFile result in updateIndexes before passing to extractFromFile
  • registry.go: Guard against nil GetSourceFile result in createAliasResolver before calling binder.BindSourceFile (the existing nil filter at lines 1080-1082 only runs after the crash)
  • investigation notes: Added investigation-nil-sourcefile-aliasresolver.md with root cause analysis covering the VFS architecture, sourceFS.FileExists two-stage check with cachedvfs fallback, symlink/realpath path mismatch theories, and dirty.SyncMap edge cases. The exact triggering condition remains uncertain.
Original prompt

This section details on the original issue you should resolve

<issue_title>nil source file in aliasResolve.GetSourceFile</issue_title>
<issue_description>## Stack trace

[13:18:24.500] Finished cloning snapshot 136 into snapshot 137 in 3.822602ms

2026-02-04 13:18:24.500 [info] 
2026-02-04 13:18:24.500 [info] Updated watches in 146.9µs
2026-02-04 13:18:24.517 [error] panic: runtime error: invalid memory address or nil pointer dereference
2026-02-04 13:18:24.517 [error] [signal SIGSEGV: segmentation violation code=0x1 addr=0x218 pc=0x67dcef]
2026-02-04 13:18:24.517 [error] 
2026-02-04 13:18:24.517 [error] goroutine 17503 [running]:
2026-02-04 13:18:24.517 [error] sync/atomic.(*Bool).Load(...)
2026-02-04 13:18:24.517 [error] 	sync/atomic/type.go:19
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/ast.(*SourceFile).IsBound(...)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/ast/ast.go:10988
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/binder.BindSourceFile(0xc02e1fd1d0?)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/binder/binder.go:96 +0xf
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/ls/autoimport.(*aliasResolver).GetSourceFile(0xc019f32280, {0xc014d71ef0, 0x44})
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/ls/autoimport/aliasresolver.go:82 +0x65
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/ls/autoimport.(*aliasResolver).GetSourceFileForResolvedModule(0xc019f32280?, {0xc014d71ef0?, 0xc004922a88?})
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/ls/autoimport/aliasresolver.go:127 +0x18
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/checker.(*Checker).resolveExternalModule(0xc037836008, 0xc005ab2a10, {0xc002acc419, 0xc}, 0x0, 0x0, 0x0)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/checker/checker.go:14799 +0x4fc
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/checker.(*Checker).resolveExternalModuleNameWorker(0xc037836008, 0xc005ab2a10, 0xc002ff9710, 0x0, 0x1, 0x0)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/checker/checker.go:14727 +0x85
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/checker.(*Checker).resolveExternalModuleName(0xc005ab2a10?, 0x0?, 0x2?, 0x0?)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/checker/checker.go:14722 +0x4a
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/checker.(*Checker).getTargetOfImportEqualsDeclaration(0xc037836008, 0xc005ab2a10)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/checker/checker.go:14066 +0x79
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/checker.(*Checker).getTargetOfAliasDeclaration(0xc037836008?, 0xc6c620?)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/checker/checker.go:15330 +0xa5
2026-02-04 13:18:24.517 [error] github.com/microsoft/typescript-go/internal/checker.(*Checker).resolveAlias(0xc037836008, 0xc00478cd58)
2026-02-04 13:18:24.517 [error] 	github.com/microsoft/typescript-go/internal/checker/checker.go:15847 +0xfb

I don't have a clear repro; I just pasted some code into a git diff viewer I was in. This took down the LS entirely and I had to manually restart it.</issue_description>

<agent_instructions>I'm not sure if you'll be able to reproduce this, but really study the code to come up with a theory of how this could happen. It's trivial to guard against, but the thing I care about is figuring out what assumptions went wrong here.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

Custom agent used: Issue Investigator
An expert at reproducing, investigating, and diagnosing issues


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Add nil check for the return value of host.GetSourceFile() before
passing it to binder.BindSourceFile(). The host can return nil when
the file doesn't exist in the current snapshot (e.g., file deleted
between snapshots, or module resolution resolved to a non-existent
path during editing).

Also add nil guard in registry.go updateIndexes where the result
of GetSourceFile is used without checking for nil.

Fixes #1203

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix nil pointer dereference in aliasResolve.GetSourceFile Fix nil pointer dereference in aliasResolver.GetSourceFile Feb 18, 2026
Copilot AI requested a review from andrewbranch February 18, 2026 00:47
Copy link
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

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

@copilot your justification is a house of cards standing on one false justification: that module resolution can succeed and then the file can not exist when reading it from the same host. While this process happens during snapshot building, the FS state should be consistent after the first read of a given path. When module resolution checks if the file exists, it actually reads its contents into disk and stores the file record (see snapshotFSBuilder.FileExists). So you can't explain this behavior as a race. Once module resolution succeeds, that path is locked into both the snapshotFSBuilder state as well as the snapshotFS produced from it after finalization.

You should match my explanation against the real code to make sure I'm not making a mistake, but I think there must be another explanation.

There's also a second possible scenario: module resolution resolved to a .d.ts or other file that exists on disk but whose path normalization differs from what SnapshotFS.GetFile expects.

Don't just say something is a possibility without any effort to back it up.

Keep looking; you now have the same understanding as me of why it should work, but you haven't found a real hole.

…ourceFile call

- Rewrite investigation notes with thorough analysis of VFS architecture,
  the sourceFS.FileExists two-stage check with cachedvfs fallback, symlink
  path mismatch theories, and dirty.SyncMap edge cases.
- Fix additional nil-unsafe binder.BindSourceFile(file) call in
  registry.go:createAliasResolver (the nil filter at lines 1080-1082
  handles nil entries but only after the crash).
- Investigation is honest about incomplete understanding: the exact
  triggering condition is uncertain, but several plausible theories
  involving stale cachedvfs caches and path normalization are documented.

Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 18, 2026

@copilot your justification is a house of cards standing on one false justification: that module resolution can succeed and then the file can not exist when reading it from the same host. While this p...

You're right — the race explanation doesn't hold because snapshotFSBuilder.FileExists reads and caches the file. I've rewritten the investigation notes in 720f0d6 with a deeper analysis.

The most interesting path I found is the sourceFS.FileExists two-stage check: when autoImportBuilderFS.GetFile returns nil, it falls back to snapshotFSBuilder.fs.FileExists() which goes to the cachedvfs. Since cachedvfs.FileExists is cached but cachedvfs.ReadFile is NOT cached (always passes through), a stale fileExistsCache entry could cause FileExists to return true while ReadFile fails. This could happen if a file was deleted from disk (e.g., during npm install) after its existence was cached.

However, tracing the dirty.SyncMap behavior shows that even after a dirty entry is deleted during reloadEntryIfNeeded, the next Load falls back to the base map (from the previous snapshot), which would return the stale file handle. So the scenario doesn't produce nil as cleanly as expected.

The symlink path mismatch theory is also interesting — module resolution's FileExists checks the original symlink path, but ResolvedFileName is the realpath. The untrackedFiles cache would have the symlink path key, not the realpath key. But a fresh ReadFile at the realpath should succeed if the file exists.

I also found and fixed an additional nil-unsafe binder.BindSourceFile(file) call in createAliasResolver (line 1074). The nil filter at lines 1080-1082 was meant to handle nil entries but only runs after the crash.

I haven't been able to pinpoint the exact triggering condition — the investigation notes document what I've found and what remains uncertain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

nil source file in aliasResolve.GetSourceFile

2 participants