sdk icon indicating copy to clipboard operation
sdk copied to clipboard

[server_plugins] Race during plugin startup/restart can result in "Null check operator used on a null value" when applying document changes

Open DanTup opened this issue 2 days ago • 2 comments

This was raised at https://github.com/Dart-Code/Dart-Code/issues/5817 by @FMorschel and I was able to reproduce it by adding an artificial delay during plugin startup.

The error occurs when the plugin is trying to apply a document change before the initial overlay has been set. It results in:

1765295969253:Ex:Null check operator used on a null value:
#0      PluginSession.sendRequest.<anonymous closure> (package::analysis_server/src/plugin/plugin_isolate.dart::392::64)

And later:

1765295982784:PluginEx:InconsistentAnalysisException:: Requested result might be inconsistent with previously returned results
:#0      AnalysisSessionImpl.checkConsistency (package::analyzer/src/dart/analysis/session.dart::58::7)
#1      AnalysisSessionImpl.getResolvedLibrary (package::analyzer/src/dart/analysis/session.dart::114::5)
#2      PluginServer.handleEditGetFixes (package::analysis_server_plugin/src/plugin_server.dart::191::62)
#3      PluginServer._getResponse (package::analysis_server_plugin/src/plugin_server.dart::607::24)

It occurs because during plugin startup, there is an async request (PluginVersionCheckParams) followed by transmission of the existing overlays. However the plugin has been created (and added to the plugin map, and has its currentSession set) before the async request, that that means it's possible for the ordering to look like this:

  • IDE is opened
  • File X is opened in IDE
  • PluginManager.addPluginToContextRoot adds plugin to _pluginMap
  • PluginManager.addPluginToContextRoot calls pluginIsolate.start
  • pluginIsolate.start sets currentSession, calls currentSession.start()
  • PluginSession.start starts an async request for PluginVersionCheckParams
  • whilst that request is in-flight, a change event arrives from the IDE for File X and is immediately forwarded to the plugin even though it does not yet have the overlays

The easiest way to repro this reliably is to just force a delay around the call to check the plugin version:

    var response = await sendRequest(
      PluginVersionCheckParams(byteStorePath, sdkPath, '1.0.0-alpha.0'),
    );
    // Simulate being slow here. We're already in pluginMap at this point
    _isolate._instrumentationService.logInfo('WAITING 20S');
    await Future.delayed(const Duration(seconds: 20));
    _isolate._instrumentationService.logInfo('FINISHED WAITING 20S');

Then ensure the file is already open at startup (VS Code will restore open files), then modify the file during the 20s delay.

I think to fix it, we need to ensure the synchronous startup code (which sends the overlays, priority files etc.) happens atomically with starting to listen to/forward the messages from the client. It's not enough to just queue the messages during startup, because we mustn't forward on a change event if it was already incorporated in the overlay that's being sent.

It looks like there is similar code in restartPlugins too, so although I didn't reproduce it, it might need changes around there too to ensure during a restart things are also set up atomically.

@srawlins FYI (I haven't tried to fix this because I'm not all that familiar with this code right now and figured you'd do a better job - but lmk if you want me to have a go)

DanTup avatar Dec 09 '25 16:12 DanTup