Skip to content

Editor SIGABRT in ScriptServer::remove_global_class_by_path under rapid EditorFileSystem::scan() (concurrent script creation) #6

@dsarno

Description

@dsarno

Summary

The editor reliably SIGABRTs in ScriptServer::remove_global_class_by_path() when many .gd files are created in rapid succession and each creation triggers EditorFileSystem::scan(). Overlapping scans re-queue the same background tasks (update_scripts_classes, update_script_paths_documentation) before the previous pass finishes, the global-class registry is left in an inconsistent state, and the next idle filesystem-scan notification aborts.

This was surfaced by a concurrency stress harness (stormtest) driving the godot-ai MCP plugin: many parallel clients call script_create (each does FileAccess write + get_resource_filesystem().scan()). It reproduces with no plugin reload involved — pure rapid scan() churn is sufficient.

Filed on my fork as a record; not yet reported upstream.

Environment

  • Godot 4.6.3.stable (mono), macOS
  • Reproduced twice, identical stack. Local crash reports:
    • ~/Library/Logs/DiagnosticReports/Godot-2026-06-01-102559.ips
    • ~/Library/Logs/DiagnosticReports/Godot-2026-05-31-195549.ips

Crash backtrace

SIGABRT (Abort trap: 6) — main thread
  abort
  ScriptServer::remove_global_class_by_path(String const&)
  EditorFileSystem::_register_global_class_script(String const&, String const&, EditorFileSystem::ScriptClassInfoUpdate const&)
  EditorFileSystem::_update_script_classes()
  EditorFileSystem::_update_scan_actions()
  EditorFileSystem::_notification(int)
  Object::_notification_forward(int)
  SceneTree::_process_group(SceneTree::ProcessGroup*, bool)

Pre-crash editor errors (the tell)

ERROR: Task 'update_scripts_classes' already exists.
ERROR: Task 'update_script_paths_documentation' already exists.
ERROR: Condition "!tasks.has(p_task)" is true.
[2] ScriptServer::remove_global_class_by_path(String const&) (in Godot) + 44

Also observed during the same window: UndoRedo history mismatch: expected 0, got 2.

Repro (high level)

  1. Run an editor with a tool that issues many EditorFileSystem::scan() calls in quick succession — e.g. the godot-ai plugin under the stormtest harness: 8 concurrent MCP clients, each rapidly calling script_create (writes a .gd then scan()).
  2. Within ~150–200 rapid file writes the editor aborts with the stack above. No plugin reload, no play-mode — just the idle filesystem scan re-entering while scan tasks are still queued.

Reproduces on both main and a feature branch of godot-ai, so it is not specific to any plugin change — it tracks the rate of scan() calls, not plugin logic.

Suspected cause

EditorFileSystem::scan() (and/or the WorkerThreadPool tasks it enqueues — update_scripts_classes, update_script_paths_documentation) is not safe to call again while a previous scan's actions are still pending. The duplicate-task guard fires (Task ... already exists / !tasks.has(p_task)), the scan-action / global-class update proceeds against an inconsistent registry, and _update_script_classes()remove_global_class_by_path() aborts.

A defensive guard (skip/queue a scan() when is_scanning() is already true, or make the global-class update idempotent against duplicate task enqueues) would likely prevent the abort.

Workaround (consumer side)

For tools that create files, prefer EditorFileSystem::update_file(path) (single-file registration) over a full scan() per write — it avoids the recursive scan-action batch entirely. (This is the mitigation being applied in the godot-ai plugin, whose script_create was the only hot path still calling full scan() per write.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions