Bevy version and features
- Bevy commit:
e8b3598ff5e5ec40e8ba84edd5750a1c0e4d4e59
- The issue requires the
multi_threaded feature.
- I reproduced it with a standalone crate using the following local Bevy features:
[package]
name = "poc"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
bevy_app = { path = "../bevy/crates/bevy_app", default-features = false, features = ["std"] }
bevy_ecs = { path = "../bevy/crates/bevy_ecs", default-features = false, features = ["std"] }
bevy_tasks = { path = "../bevy/crates/bevy_tasks", default-features = false, features = ["async_executor", "multi_threaded"] }
bevy_transform = { path = "../bevy/crates/bevy_transform", default-features = false, features = ["std", "bevy-support", "bevy_reflect", "async_executor", "multi_threaded"] }
[Optional] Relevant system information
If you cannot get Bevy to build or run on your machine, please include:
- the Rust version I'm using:
- cargo 1.97.1 (c980f4866 2026-06-30)
- rustc 1.97.1 (8bab26f4f 2026-07-14)
- OS: Windows 11 version
10.0.26200
What you did
I created a hierarchy where child has its own child, then used the safe replace_children API to replace an existing Children collection with 1024 copies of that same non-leaf child.
use bevy_app::App;
use bevy_ecs::prelude::*;
use bevy_tasks::{ComputeTaskPool, TaskPoolBuilder};
use bevy_transform::{
components::{GlobalTransform, Transform},
TransformPlugin,
};
const DUPLICATE_COUNT: usize = 1024;
fn main() {
// Use two workers so separate work batches can run concurrently.
ComputeTaskPool::get_or_init(|| TaskPoolBuilder::new().num_threads(2).build());
let mut app = App::new();
app.add_plugins(TransformPlugin);
let grandchild = app
.world_mut()
.spawn(Transform::from_xyz(0.0, 1.0, 0.0))
.id();
let child = app
.world_mut()
.spawn(Transform::from_xyz(1.0, 0.0, 0.0))
.id();
// Make `child` a non-leaf node.
app.world_mut().entity_mut(child).add_child(grandchild);
let root = app
.world_mut()
.spawn(Transform::from_xyz(10.0, 0.0, 0.0))
.id();
// This is required to enter replace_related's existing-collection branch.
app.world_mut().entity_mut(root).add_child(child);
// Safe API: creates 1024 duplicate Children entries.
app.world_mut()
.entity_mut(root)
.replace_children(&vec![child; DUPLICATE_COUNT]);
let children = app.world().get::<Children>(root).unwrap();
assert_eq!(children.len(), DUPLICATE_COUNT);
assert!(children.iter().all(|entity| entity == child));
app.update();
}
In my Windows PC, I run the above code by using the following command:
$env:MIRIFLAGS = "-Zmiri-preemption-rate=1 -Zmiri-seed=1"
cargo +nightly-x86_64-pc-windows-gnu miri run
What went wrong
Miri reports UB:
error: Undefined Behavior: Data race detected between (1) retag write on thread `main` and (2) retag write of type `bevy_transform::components::GlobalTransform` on thread `TaskPool (0)` at alloc427219
--> D:\projects\bevy\crates\bevy_ptr\src\lib.rs:1253:18
|
1253 | unsafe { &mut *self.get() }
| ^^^^^^^^^^^^^^^^ (2) just happened here
|
help: and (1) occurred earlier here
--> src\main.rs:48:5
|
48 | app.update();
| ^^^^^^^^^^^^
= help: retags occur on all (re)borrows and as well as when references are copied or moved
= help: retags permit optimizations that insert speculative reads or writes
= help: therefore from the perspective of data races, a retag has the same implications as a read or write
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: this is on thread `TaskPool (0)`
note: the last function in that backtrace got called indirectly due to this code
--> src\main.rs:13:37
|
13 | ..._init(|| TaskPoolBuilder::new().num_threads(2).build());
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 1 previous error; 1 warning emitted
error: process didn't exit successfully: `C:\Users\14798\.rustup\toolchains\nightly-x86_64-pc-windows-gnu\bin\cargo-miri.exe runner target\miri\x86_64-pc-windows-gnu\debug\c15-miri-demo.exe` (exit code: 1)
I found that replace_children accepts duplicate entities when the parent already has a Children component.
The relevant path appears to be:
-
EntityWorldMut::replace_children calls replace_related::<ChildOf>.
-
replace_related uses an EntityHashSet when updating ChildOf, but later copies the original input slice directly into Children:
In EntityWorldMut::replace_related:
collection.clear();
collection.extend_from_iter(related.iter().copied());
- The multi-threaded transform system later treats Children as unique without validating it:
In system.rs propagate_descendants_unchecked:
UniqueEntitySlice::from_slice_unchecked(p_children)
-
Each duplicate non-leaf child is queued as a transform propagation task. The work queue batches tasks in groups of 512, so 1024 duplicates can be processed by separate workers.
-
Workers access the duplicate task entity using nodes.get_unchecked(parent), obtaining mutable GlobalTransform access under the assumption that each task is a disjoint subtree.
Additional information
The backtrace stack is so long that I think it is better not to paste here.
The issue is not limited to the direct replace_children. replace_related_with_difference documents that its input slices must not contain duplicates, but its duplicate validation is currently only enabled under debug_assertions. A release build can therefore potentially construct duplicate Children state through that path as well.
A possible fix would be to either:
- deduplicate entities in safe APIs, preserving first-occurrence order; or
- reject duplicates consistently in all build modes.
Bevy version and features
e8b3598ff5e5ec40e8ba84edd5750a1c0e4d4e59multi_threadedfeature.[Optional] Relevant system information
If you cannot get Bevy to build or run on your machine, please include:
10.0.26200What you did
I created a hierarchy where child has its own child, then used the safe replace_children API to replace an existing Children collection with 1024 copies of that same non-leaf child.
In my Windows PC, I run the above code by using the following command:
What went wrong
Miri reports UB:
I found that
replace_childrenaccepts duplicate entities when the parent already has aChildrencomponent.The relevant path appears to be:
EntityWorldMut::replace_childrencallsreplace_related::<ChildOf>.replace_relateduses anEntityHashSetwhen updatingChildOf, but later copies the original input slice directly intoChildren:In
EntityWorldMut::replace_related:In system.rs
propagate_descendants_unchecked:Each duplicate non-leaf child is queued as a transform propagation task. The work queue batches tasks in groups of 512, so 1024 duplicates can be processed by separate workers.
Workers access the duplicate task entity using
nodes.get_unchecked(parent), obtaining mutableGlobalTransformaccess under the assumption that each task is a disjoint subtree.Additional information
The backtrace stack is so long that I think it is better not to paste here.
The issue is not limited to the direct
replace_children.replace_related_with_differencedocuments that its input slices must not contain duplicates, but its duplicate validation is currently only enabled under debug_assertions. A release build can therefore potentially construct duplicateChildrenstate through that path as well.A possible fix would be to either: