Skip to content

[Bug]: OnValidate Crashes #210

@wallstop

Description

Description

DxMessaging OnValidate Reentrant-Import Crash � Root Cause and Fix Spec

Summary

The Unity Editor crashes natively in GuidReservations::Reserve (reached via
ImportAtPathV2) on domain load, reproduced on Unity 6000.4.5f1. The root
cause is a reentrant AssetDatabase.ImportAsset call originating from
ScriptableObject.OnValidate
, which Unity invokes synchronously within the
asset import/load operation for the settings asset during domain-load
initialization, before that operation returns.

  • Affected package: com.wallstop-studios.dxmessaging
  • Affected version: 3.0.1
  • Package cache hash referenced in this document: 57ed15f89e13
  • Crash class: native editor crash (not a managed exception � the
    try/catch in TryRegenerateSidecar cannot catch it)

This is a hand-off artifact for the separate DxMessaging package repository. All
file:line references below are pinned to v3.0.1 / package cache hash
57ed15f89e13; the maintainer should re-confirm them against current main
before applying the fix.


Environment / Crash Signature

Field Value
Unity Editor 6000.4.5f1
Platform Reproduced on the editor (Linux CI editor image and desktop editors)
Trigger Domain load / assembly reload while Assets/Editor/DxMessagingSettings.asset exists on disk
Native frame GuidReservations::Reserve
Native caller AssetDatabase import path ImportAtPathV2
Managed entry point DxMessagingEditorInitializer static constructor ([InitializeOnLoad])

The crash is a hard native abort. It happens during domain reload, so it is
not catchable by the managed try/catch in
DxMessagingSettings.TryRegenerateSidecar � the process is gone before control
returns to managed code.


Verified Call Chain

Confirmed against the package source at cache hash 57ed15f89e13, v3.0.1:

  1. DxMessagingEditorInitializer..cctorEditor/DxMessagingEditorInitializer.cs:18
    (the [InitializeOnLoad] attribute is at Editor/DxMessagingEditorInitializer.cs:13).
    Runs on every domain load.
  2. ApplyEditorSettings() � called at Editor/DxMessagingEditorInitializer.cs:21;
    method defined at Editor/DxMessagingEditorInitializer.cs:48.
  3. DxMessagingSettings.GetOrCreateSettings() � called at
    Editor/DxMessagingEditorInitializer.cs:51; method defined at
    Editor/Settings/DxMessagingSettings.cs:163.
  4. AssetDatabase.LoadAssetAtPath<DxMessagingSettings>(...)
    Editor/Settings/DxMessagingSettings.cs:165. Loading the asset
    deserializes it, and Unity fires OnValidate synchronously within that
    load operation, before it returns.
  5. DxMessagingSettings.OnValidate()Editor/Settings/DxMessagingSettings.cs:229.
  6. TryRegenerateSidecar() � called at Editor/Settings/DxMessagingSettings.cs:235;
    method defined at Editor/Settings/DxMessagingSettings.cs:291.
  7. DxMessagingBaseCallIgnoreSync.RegenerateSidecar(this) � called at
    Editor/Settings/DxMessagingSettings.cs:295; method defined at
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:48. Its guard
    if (EditorApplication.isUpdating || EditorApplication.isCompiling) is at
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:55.
  8. RegenerateSidecarCore(settings) � called at
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:57 (deferred) and
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:61 (synchronous); method
    defined at Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:64.
  9. File.WriteAllText(...) at Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:86
    + AssetDatabase.ImportAsset(SidecarAssetPath) at
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:87.
  10. native crash in GuidReservations::Reserve via ImportAtPathV2 � the
    ImportAsset call is reentrant with the in-flight asset import /
    deserialization that LoadAssetAtPath is still inside.

Root Cause

OnValidate is a serialization-time callback. When
AssetDatabase.LoadAssetAtPath deserializes DxMessagingSettings.asset during
domain-load initialization, Unity invokes OnValidate after deserialization
completes but still synchronously within the asset import/load operation's
scope, before that operation returns. OnValidate then synchronously walks down
to AssetDatabase.ImportAsset, which re-enters the asset import subsystem while
an import is already in flight.

Unity 6000.4 added stricter reentrancy guards in the native import path. A
reentrant import is no longer tolerated: GuidReservations::Reserve aborts the
process instead of logging an error. The same code path was merely fragile on
earlier Unity versions; on 6000.4 it is a hard crash.

Why the existing two-flag guard is insufficient

RegenerateSidecar guards with:

if (EditorApplication.isUpdating || EditorApplication.isCompiling)
{
    EditorApplication.delayCall += () => RegenerateSidecarCore(settings);
    return;
}
RegenerateSidecarCore(settings);

(Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:55-61)

This is insufficient for two reasons:

  1. The flags do not cover the crash window. EditorApplication.isUpdating
    and EditorApplication.isCompiling do not reliably report true during
    the domain-load / asset-import-worker phase in which OnValidate fires here.
    With both flags false, the guard takes the else branch and calls
    RegenerateSidecarCore � and therefore AssetDatabase.ImportAsset
    synchronously, inside the deserialization callback. That is the reentrant
    import that crashes.
  2. The deferred closure captures state across a reload boundary. Even when
    the guard does defer, the lambda () => RegenerateSidecarCore(settings)
    captures the settings object. EditorApplication.delayCall runs after the
    domain reload completes; by then the captured settings reference may have
    been invalidated by the reload, and the closure is never unsubscribed, so
    repeated OnValidate invocations can stack duplicate one-shots.

In short: gating the deferral decision on isUpdating/isCompiling is the bug
� those flags are not a valid proxy for "is it safe to mutate the
AssetDatabase". The only safe place to check editor state is inside the
deferred one-shot when it actually runs
, not at the point of scheduling.


Recommended Fix

Apply all of the following in the DxMessaging package:

  1. Remove the AssetDatabase mutation from the OnValidate path entirely.
    OnValidate (Editor/Settings/DxMessagingSettings.cs:229) must not lead �
    directly or transitively � to AssetDatabase.ImportAsset,
    AssetDatabase.SaveAssets, AssetDatabase.CreateAsset, or any other
    AssetDatabase mutation. The same applies to AddIgnoredType /
    RemoveIgnoredType if they can run inside an import window.
  2. Trigger sidecar regeneration only from a safe context: either an explicit
    user action (a menu item / Project Settings button), or a single
    post-domain-reload EditorApplication.delayCall one-shot that re-checks
    editor state when it runs
    .
  3. Replace the two-flag guard with unconditional deferral. In
    RegenerateSidecar (Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:48-62),
    do not branch on isUpdating/isCompiling to decide whether to defer �
    always defer. Re-check isUpdating/isCompiling (and settings != null)
    inside the deferred one-shot, and bail out if the editor is still in an
    unsafe state; a later user action or the next callback will retry.
  4. Subscribe a named method, not a closure. Use a named one-shot that
    unsubscribes itself (delayCall -= Handler; delayCall += Handler; then
    delayCall -= Handler; as the first line of the handler) so duplicate
    one-shots cannot stack across reload boundaries and no stale settings
    reference is captured.
  5. Reconsider whether AssetDatabase.ImportAsset is needed at all. The
    sidecar (Assets/Editor/DxMessaging.BaseCallIgnore.txt,
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:27) is consumed by the
    Roslyn analyzer via csc.rsp -additionalfile. A plain File.WriteAllText
    plus a lazy import (let Unity pick up the change on its next refresh) is very
    likely sufficient; the explicit ImportAsset at
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:87 may be unnecessary
    churn. Only call ImportAsset if the sidecar genuinely must be re-imported
    immediately � and even then, only from a safe (non-callback) context.
  6. Keep the existing content-diff short-circuit. The
    File.ReadAllText / string.Equals comparison at
    Editor/Settings/DxMessagingBaseCallIgnoreSync.cs:77-84 is correct and worth
    keeping � it avoids writing (and importing) when nothing changed. Retain it.

Validation / Repro Steps

To reproduce the crash on an unpatched v3.0.1:

  1. Use Unity 6000.4.5f1 with com.wallstop-studios.dxmessaging 3.0.1.
  2. Ensure Assets/Editor/DxMessagingSettings.asset exists on disk (open the
    project once so GetOrCreateSettings creates it).
  3. Force a domain reload � e.g. trigger a script recompile, or use
    Assets > Refresh, or restart the editor.
  4. Observe the native crash in GuidReservations::Reserve (ImportAtPathV2)
    during domain-load initialization.

To validate the fix:

  1. Apply the recommended fix above.
  2. Repeat steps 1�3. The editor must complete domain reload with no native
    crash.
  3. Edit the ignored-types list via the Project Settings UI and confirm the
    sidecar (Assets/Editor/DxMessaging.BaseCallIgnore.txt) is still regenerated
    correctly � just from a safe context, not from inside OnValidate.
  4. Confirm the content-diff short-circuit still suppresses redundant writes
    (editing then reverting a value should not rewrite the sidecar).
  5. Confirm no duplicate delayCall one-shots stack up across repeated reloads.

References

  • .llm/skills/defensive-editor-programming.md
    � section "Never Mutate AssetDatabase from Validation Callbacks" captures this
    pattern as a reusable rule.
  • Package source (cache hash 57ed15f89e13, v3.0.1):
    • Editor/DxMessagingEditorInitializer.cs
    • Editor/Settings/DxMessagingSettings.cs
    • Editor/Settings/DxMessagingBaseCallIgnoreSync.cs

Note: All file:line references in this document are pinned to
DxMessaging v3.0.1 / package cache hash 57ed15f89e13. Line numbers
drift between releases � the maintainer should re-confirm every reference
against the current main branch before applying the fix.

Steps to Reproduce

The stuff above

Expected Behavior

Unity happy

Actual Behavior

Unity Sad

Unity Version

6000.x

Package Version

3.0.1

Platform

  • Editor
  • Windows
  • macOS
  • Linux
  • iOS
  • Android
  • WebGL
  • Other

Additional Context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions