Skip to content

bevy-ui: Fix crash when despawning a Node that had its ChildOf removed. - #25386

Open
CyberspaceDreamn wants to merge 5 commits into
bevyengine:mainfrom
CyberspaceDreamn:main
Open

bevy-ui: Fix crash when despawning a Node that had its ChildOf removed.#25386
CyberspaceDreamn wants to merge 5 commits into
bevyengine:mainfrom
CyberspaceDreamn:main

Conversation

@CyberspaceDreamn

Copy link
Copy Markdown

Objective

Prevent an occasional crash in taffy / bevy_ui when despawning Nodes that had their ChildOf component removed.

Fixes #25303. 1

Solution

When a Node has its ChildOf removed, also remove its TaffyTree::parent. I believe this prevents bevy from calling TaffyTree::remove twice with the same NodeId.

Testing

  • bevy_ui unit tests 2
  • a mostly reliable demo of this crash (link) 2

Footnotes

  1. I cannot replicate the crash with this patch applied, but solid proof is difficult with heisenbugs.

  2. Tested only on Linux, but as this is a logic bug platform should not matter. 2

@github-actions

Copy link
Copy Markdown
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide, as well as our policy regarding AI usage, and we look forward to reviewing your pull request shortly ✨

@kfc35 kfc35 added C-Bug An unexpected or incorrect behavior A-UI Graphical user interfaces, styles, layouts, and widgets S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 13, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in UI Aug 13, 2026
@kfc35
kfc35 requested a review from ickshonpe August 13, 2026 03:19
Comment thread crates/bevy_ui/src/layout/mod.rs Outdated
Comment on lines +152 to +170
// Remove parents of root nodes to avoid a potential panic (invalid SlotMap key used).
for root in removed_parents.read() {
if !node_query.contains(root) {
continue;
}

let Some(taffy_node) = ui_surface.entity_to_taffy.get(&root) else {
continue;
};

let taffy_node = taffy_node.id;

let Some(taffy_parent) = ui_surface.taffy.parent(taffy_node) else {
continue;
};

let _ = ui_surface.taffy.remove_child(taffy_parent, taffy_node);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes the new root from the previous parent's children, before the implicit viewport node is added. So then in update_children_recursively set_children for the previous parent doesn't see the root and doesn't overwrite the root's implicit viewport node parent. I think this works, but there mght be a shorter fix.

@ickshonpe ickshonpe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying problem is that the new root is given an implicit taffy parent representing the viewport in get_or_insert_taffy_viewport, but its not removed from its old parent's children at the same time. Then when the old parent's children are updated it sees the new root was removed and clears the new root's parent, which it expects to be the old parent, but it isn't, it's the new implicit viewport parent.

So it should be sufficient to just remove the new root node from its previous parent's (if it had a previous parent) taffy children in get_or_insert_taffy_viewport. If that makes sense.

CyberspaceDreamn and others added 3 commits August 13, 2026 15:55
# Objective

Fixes bevyengine#25378

## Solution

As I understand, Bevy ECS uses local thread occupancy marker for systems
that must run on the local thread. This would be exclusive systems (no
other system runs) or non-send systems (can not move off the ECS "main"
thread).

The executor marked the local thread as free after non-send systems, but
not after send exclusive systems.

This fix clears the `local_thread_running` flag now after exclusive
system has ran, in addition to the previous non-send clearance.

## Testing

- The PR contains a regression test using exclusive-system followed by
non-send system.
- Verified that both tests fail without the fix.
- Ran the full bevy ECS test suite with the fix, passes.

AI disclosure: the bug debugging & fix suggestion + code suggestion for
#[test]'s was done by AI.
@CyberspaceDreamn

CyberspaceDreamn commented Aug 13, 2026

Copy link
Copy Markdown
Author

You are mostly correct, but it is the root being present in multiple children, not the missing TaffyTree::parent that causes this crash.

from taffy/src/taffy_tree.rs:631-635

if let Some(children) = self.children.get(key) {
    for child in children.iter().copied() {
        self.parents[child.into()] = None; // crashes here with `invalid SlotMap key used`
    }
}

If self.parents[child.into()] still existed here, assigning to it would not be accessing an invalid slotmap key. Despawning the Node will also crash even if it has been several seconds after removing its ChildOf, so update order within one frame is not the cause of this crash.

I believe the root cause is an unresolved bug in TaffyTree::add_child DioxusLabs/taffy#1100. bevy's update order seems to partially mitigate that issue & explains the inconsistency of the crash.

Either way, explicitly removing its previous parent in get_or_insert_taffy_viewport, as you suggested, does prevent bevy from crashing until/unless a patch lands upstream.

@ickshonpe ickshonpe added the P-Crash A sudden unexpected crash label Aug 14, 2026
@ickshonpe ickshonpe added this to the 0.19.2 milestone Aug 14, 2026
@ickshonpe

ickshonpe commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

This seems to work reliably as a regression test:

    #[test]
    fn node_with_sibling_should_have_implicit_viewport_node_parent_after_unparenting() {
        #[derive(Component)]
        struct Marker;
        let mut app = setup_ui_test_app();
        let world = app.world_mut();
        world.spawn(Node::default());
        let child_a = world.spawn(Node::default()).id();
        let child_b = world.spawn(Node::default()).id();
        world
            .spawn((Node::default(), Marker))
            .add_children(&[child_a, child_b]);
        app.update();
        app.world_mut().entity_mut(child_a).remove::<ChildOf>();
        app.update();
        let ui_surface = app.world().resource::<UiSurface>();
        let node = ui_surface.entity_to_taffy[&child_a];
        assert_eq!(ui_surface.taffy.parent(node.id), node.viewport_id);
        assert!(ui_surface
            .taffy
            .children(node.viewport_id.unwrap())
            .is_ok_and(|children| children.contains(&node.id)));
    }

It makes assumptions about archetypes and query ordering etc that might not hold in later bevy versions though.

@gagnus

gagnus commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

it looks right to me but I'm not experienced enough with taffy to say for sure.

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

Labels

A-UI Graphical user interfaces, styles, layouts, and widgets C-Bug An unexpected or incorrect behavior P-Crash A sudden unexpected crash S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

bevy-ui / Taffy crashes when un-parenting and later despawning a Node

6 participants