Skip to content

API: snapshot.update with both removePrograms and createPrograms crashes tsgo (nil dereference in ProjectCollectionBuilder.HandleAPIRequest) #64911

Open

Description

🔎 Search Terms

removePrograms, createPrograms, synthetic program, snapshot update, HandleAPIRequest, nil pointer dereference, SyncMapEntry, typescript/sync, API crash, panic

🕗 Version & Regression Information

  • This is the behavior in every version I tried: 7.1.0-beta and 7.1.0-dev.20261021.1 (typescript@next)
  • I was unable to test this on prior versions because the createPrograms / removePrograms snapshot API is new in 7.1

This was found working on the migration of ts-loader to TypeScript 7.1: TypeStrong/ts-loader@d159d32

⏯ Playground Link

N/A - this is the typescript/sync API, not the language/checker, so it can't be shown in the Playground. A self-contained repro script is below.

💻 Code

// repro.mjs - run with: node repro.mjs (typescript@beta or typescript@next installed)
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { API } from "typescript/sync";

const dir = fs.mkdtempSync(path.join(os.tmpdir(), "remove-programs-"));
const a = path.join(dir, "a.ts");
const b = path.join(dir, "b.ts");
fs.writeFileSync(a, "export const a = 1;\n");
fs.writeFileSync(b, "export const b = 2;\n");

const api = new API();
const snapshot = api.createSnapshot({
  createPrograms: [{ rootFiles: [a], compilerOptions: {} }],
});
const first = snapshot.operation.createdPrograms[0].id;

// Remove the first synthetic program and create another in the same update.
const next = snapshot.update({
  removePrograms: [first],
  createPrograms: [{ rootFiles: [b], compilerOptions: {} }],
});

console.log(next.operation.createdPrograms[0].getSemanticDiagnostics(b).length);

🙁 Actual behavior

The tsgo server process panics, and the client then fails with Error: Unexpected EOF while reading from child process (unknown reason). Every later request on the same API instance fails too (in a webpack build these surface as EPIPE: broken pipe / EBADF: bad file descriptor).

panic: runtime error: invalid memory address or nil pointer dereference [recovered, repanicked]
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x100f97a74]

goroutine 196 [running]:
sync.(*WaitGroup).Go.func1.1()
	sync/waitgroup.go:251 +0x48
panic({0x101d28ba0?, 0x101e7a190?})
	runtime/panic.go:859 +0x120
github.com/microsoft/TypeScript/tsc/internal/project/dirty.(*SyncMapEntry[...]).Value(0x43cf245acd08?)
	github.com/microsoft/TypeScript/tsc/internal/project/dirty/syncmap.go:58 +0x24
github.com/microsoft/TypeScript/tsc/internal/project.(*ProjectCollectionBuilder).HandleAPIRequest.func2()
	github.com/microsoft/TypeScript/tsc/internal/project/projectcollectionbuilder.go:347 +0x4c
sync.(*WaitGroup).Go.func1()
	sync/waitgroup.go:258 +0x48
created by sync.(*WaitGroup).Go in goroutine 1

(Trace from 7.1.0-dev.20261021.1; 7.1.0-beta panics at the same frames.)

🙂 Expected behavior

The update succeeds: the first synthetic program is removed and the second is created, and the script prints 0.

Making the same two changes as separate updates works on both versions, which is the behavior I'd expect from the combined request too:

const removed = snapshot.update({ removePrograms: [first] });
snapshot.dispose();
const next = removed.update({
  createPrograms: [{ rootFiles: [b], compilerOptions: {} }],
});
// prints 0

Additional information about the issue

  • Environment: macOS (Darwin arm64), Node.js v24.18.0.
  • Found while integrating the 7.1 API into ts-loader (Support TypeScript 7.1 API TypeStrong/ts-loader#1704), which keeps a bounded set of synthetic programs and evicts the oldest when adding a new one - so any project that needed more than the cap of synthetic programs crashed the compiler. ts-loader now sends the removal as its own update as a workaround.
  • No tsconfig.json, configured project or openFiles is needed to reproduce: two synthetic programs are enough.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions