Blender may crash with RecursionError when deleting multiple objects/armatures (race condition) #431

Closed
opened 2025-12-09 06:45:51 +00:00 by Ghost · 2 comments
Ghost commented 2025-12-09 06:45:51 +00:00 (Migrated from git.disroot.org)

Description

Blender 5.0 crashes with a RecursionError when deleting multiple objects containing armatures. The crash does not occur when the plugin is disabled.

Crash is preceded by:

WARNING current value '0' matches no enum in 'Scene', 'Scene', 'armature'

Then hundreds of stack frames:

RecursionError: maximum recursion depth exceeded
File ".../cats_blender_plugin/tools/common.py", line 2392, in wrapped_items_func
File ".../cats_blender_plugin/tools/common.py", line 2395, in wrapped_items_func
File ".../cats_blender_plugin/tools/common.py", line 468, in get_armature_list

Full crash log: https://gist.github.com/lazulit3/2f89729ba0f3f3d5ebc849016166265a

Blender Version

  • Blender 5.0.0

CATS Version

  • Tested on 5.0.1 and 5.0.2

Operating System

  • Windows 11
  • macOS Sequoia (15.x)

Steps to Reproduce

Note: This bug is difficult to reproduce in synthetic test cases but consistently occurs in a real VRChat avatar project that I'm working with.

Confirmed Reproduction

  1. Open a VRChat avatar project with multiple armatures (main avatar + accessories)
  2. Select multiple objects/armatures
  3. Delete them (X key or "Delete Hierarchy")
  4. Blender freezes and crashes

Consistently reproducible with Skip4D's Chibi Base Avatar (Deluxe). Unfortunately I cannot share files for reproducing this, as I cannot redistribute their work. But I'm happy to help test iterations of a fix if that's helpful.

Attempted Minimal Test Case (Unsuccessful)

I attempted to create a minimal reproducible test case:

  • 1 main avatar armature (12 bones) + body mesh
  • 24 accessory objects with individual armatures
  • Total: 25 armatures, 50 objects, 49 bones
  • All accessories with armature modifiers referencing main avatar bones
  • Organized in separate collections

Despite matching the structure of real projects where the crash occurs, this synthetic test case did not trigger the bug. The crash appears to require specific conditions present in real-world avatar projects that are difficult to isolate, suggesting a race condition.

Root Cause

The Blender 5.0 enum compatibility code has infinite recursion in tools/common.py:

  1. Armatures are deleted → bpy.context.view_layer.objects is modified
  2. Blender UI reads context.scene.armature EnumProperty
  3. wrapped_items_func() detects out-of-bounds value
  4. _schedule_enum_fix() attempts direct setattr() (line 2343)
  5. Blender re-validates enum → calls wrapped_items_func() again
  6. Infinite loop → crash at ~1000 recursion depth

Problematic code:

def _schedule_enum_fix(property_holder, scene, property_name, property_path, new_value):
    try:
        setattr(property_holder, property_name, new_value)  # ← CAUSES RECURSION
        return
    except (AttributeError, TypeError, RuntimeError):
        pass

Proposed Solution

Primary Fix (Option 1): Remove direct setattr() and always use timer-based updates:

def _schedule_enum_fix(property_holder, scene, property_name, property_path, new_value):
    # Always schedule via timer to avoid recursion
    scene_name = scene.name
    scheduled_property_set = _enum_choice_fix_scheduled.setdefault(scene_name, set())

    if property_path in scheduled_property_set:
        return

    scheduled_property_set.add(property_path)

    def fix_enum_task():
        scene_by_name = bpy.data.scenes.get(scene_name)
        if scene_by_name:
            try:
                prop = scene_by_name.path_resolve(property_path, False)
                setattr(prop.data, property_name, new_value)
            except:
                try:
                    setattr(scene_by_name, property_name, new_value)
                except:
                    pass
        scheduled_property_set.discard(property_path)
        return None

    bpy.app.timers.register(fix_enum_task, first_interval=0.0)

Defense-in-Depth (Option 2): Add recursion guard to wrapped_items_func():

_enum_items_being_processed = {}

def wrapped_items_func(self, context):
    holder_id = id(self)
    property_set = _enum_items_being_processed.setdefault(holder_id, set())

    if property_name in property_set:
        return [(_empty_enum_identifier, "Loading...", "")]

    try:
        property_set.add(property_name)
        # ... original logic ...
    finally:
        property_set.discard(property_name)
        if not property_set:
            _enum_items_being_processed.pop(holder_id, None)

Additional Context

  • Specific to Blender 5.0+ due to EnumProperty integer index changes
  • Previous commit 3e1af6b partially fixed enum handling but left the problematic setattr()
  • tools/common.py: Lines 2339, 2392, 2248
  • extentions.py: Line 57

References

## Description Blender 5.0 crashes with a `RecursionError` when deleting multiple objects containing armatures. The crash does not occur when the plugin is disabled. Crash is preceded by: ``` WARNING current value '0' matches no enum in 'Scene', 'Scene', 'armature' ``` Then hundreds of stack frames: ``` RecursionError: maximum recursion depth exceeded File ".../cats_blender_plugin/tools/common.py", line 2392, in wrapped_items_func File ".../cats_blender_plugin/tools/common.py", line 2395, in wrapped_items_func File ".../cats_blender_plugin/tools/common.py", line 468, in get_armature_list ``` Full crash log: https://gist.github.com/lazulit3/2f89729ba0f3f3d5ebc849016166265a ## Blender Version - Blender 5.0.0 ## CATS Version - Tested on 5.0.1 and 5.0.2 ## Operating System - Windows 11 - macOS Sequoia (15.x) ## Steps to Reproduce **Note:** This bug is difficult to reproduce in synthetic test cases but consistently occurs in a real VRChat avatar project that I'm working with. ### Confirmed Reproduction 1. Open a VRChat avatar project with multiple armatures (main avatar + accessories) 2. Select multiple objects/armatures 3. Delete them (X key or "Delete Hierarchy") 4. Blender freezes and crashes Consistently reproducible with [Skip4D's Chibi Base Avatar (Deluxe)](https://skip4d.gumroad.com/l/chibi). Unfortunately I cannot share files for reproducing this, as I cannot redistribute their work. But I'm happy to help test iterations of a fix if that's helpful. ### Attempted Minimal Test Case (Unsuccessful) I attempted to create a minimal reproducible test case: - 1 main avatar armature (12 bones) + body mesh - 24 accessory objects with individual armatures - Total: 25 armatures, 50 objects, 49 bones - All accessories with armature modifiers referencing main avatar bones - Organized in separate collections Despite matching the structure of real projects where the crash occurs, this synthetic test case did not trigger the bug. The crash appears to require specific conditions present in real-world avatar projects that are difficult to isolate, suggesting a race condition. ## Root Cause The Blender 5.0 enum compatibility code has infinite recursion in `tools/common.py`: 1. Armatures are deleted → `bpy.context.view_layer.objects` is modified 2. Blender UI reads `context.scene.armature` EnumProperty 3. `wrapped_items_func()` detects out-of-bounds value 4. `_schedule_enum_fix()` attempts direct `setattr()` (line 2343) 5. Blender re-validates enum → calls `wrapped_items_func()` again 6. **Infinite loop** → crash at ~1000 recursion depth Problematic code: ```python def _schedule_enum_fix(property_holder, scene, property_name, property_path, new_value): try: setattr(property_holder, property_name, new_value) # ← CAUSES RECURSION return except (AttributeError, TypeError, RuntimeError): pass ``` ## Proposed Solution **Primary Fix (Option 1):** Remove direct `setattr()` and always use timer-based updates: ```python def _schedule_enum_fix(property_holder, scene, property_name, property_path, new_value): # Always schedule via timer to avoid recursion scene_name = scene.name scheduled_property_set = _enum_choice_fix_scheduled.setdefault(scene_name, set()) if property_path in scheduled_property_set: return scheduled_property_set.add(property_path) def fix_enum_task(): scene_by_name = bpy.data.scenes.get(scene_name) if scene_by_name: try: prop = scene_by_name.path_resolve(property_path, False) setattr(prop.data, property_name, new_value) except: try: setattr(scene_by_name, property_name, new_value) except: pass scheduled_property_set.discard(property_path) return None bpy.app.timers.register(fix_enum_task, first_interval=0.0) ``` **Defense-in-Depth (Option 2):** Add recursion guard to `wrapped_items_func()`: ```python _enum_items_being_processed = {} def wrapped_items_func(self, context): holder_id = id(self) property_set = _enum_items_being_processed.setdefault(holder_id, set()) if property_name in property_set: return [(_empty_enum_identifier, "Loading...", "")] try: property_set.add(property_name) # ... original logic ... finally: property_set.discard(property_name) if not property_set: _enum_items_being_processed.pop(holder_id, None) ``` ## Additional Context - Specific to Blender 5.0+ due to EnumProperty integer index changes - Previous commit 3e1af6b partially fixed enum handling but left the problematic `setattr()` ## Related Files - `tools/common.py`: Lines 2339, 2392, 2248 - `extentions.py`: Line 57 ## References - Blender 5.0 API: [EnumProperty now returns int](https://projects.blender.org/blender/blender/issues/115908)
Ghost commented 2025-12-09 10:29:41 +00:00 (Migrated from git.disroot.org)

This is a good first issue, i will review you PR fully later however it does seem like it's a good fix.

This is a good first issue, i will review you PR fully later however it does seem like it's a good fix.
Ghost commented 2025-12-09 13:57:49 +00:00 (Migrated from git.disroot.org)

You fix has been merged into the dev branch, it had been marked for the 5.0.2.1 release which will be released on Friday, thanks for your contribution.

You fix has been merged into the dev branch, it had been marked for the 5.0.2.1 release which will be released on Friday, thanks for your contribution.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Kneelawk/Cats-Blender-Plugin#431
No description provided.