Prevent TOCTOU bugs in ComponentsQueuedRegistrator (#20016)

# Objective

- Fix #20014

## Solution

- Don't make the `force_register_` family of functions always enqueue of
the component/resource; instead have them check again whether it was
already queued or not.

## Testing

- I added a small regression test but it's non-deterministic: if the bug
is fixed it will always pass, but if the bug is presen then it has a
(hopefully small) chance of passing. On my PC it failed after ~100
iterations, so hopefully 1000 is enough in CI (assuming it doesn't have
single core runners...)

---------

Co-authored-by: JaySpruce <jsprucebruce@gmail.com>
This commit is contained in:
Giacomo Stevanato 2025-07-14 22:41:18 +02:00 committed by GitHub
parent 6e3739ac96
commit 89c3a979e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 40 additions and 27 deletions

View File

@ -472,58 +472,58 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
Self { components, ids } Self { components, ids }
} }
/// Queues this function to run as a component registrator. /// Queues this function to run as a component registrator if the given
/// type is not already queued as a component.
/// ///
/// # Safety /// # Safety
/// ///
/// The [`TypeId`] must not already be registered or queued as a component. /// The [`TypeId`] must not already be registered as a component.
unsafe fn force_register_arbitrary_component( unsafe fn register_arbitrary_component(
&self, &self,
type_id: TypeId, type_id: TypeId,
descriptor: ComponentDescriptor, descriptor: ComponentDescriptor,
func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static, func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static,
) -> ComponentId { ) -> ComponentId {
let id = self.ids.next();
self.components self.components
.queued .queued
.write() .write()
.unwrap_or_else(PoisonError::into_inner) .unwrap_or_else(PoisonError::into_inner)
.components .components
.insert( .entry(type_id)
type_id, .or_insert_with(|| {
// SAFETY: The id was just generated. // SAFETY: The id was just generated.
unsafe { QueuedRegistration::new(id, descriptor, func) }, unsafe { QueuedRegistration::new(self.ids.next(), descriptor, func) }
); })
id .id
} }
/// Queues this function to run as a resource registrator. /// Queues this function to run as a resource registrator if the given
/// type is not already queued as a resource.
/// ///
/// # Safety /// # Safety
/// ///
/// The [`TypeId`] must not already be registered or queued as a resource. /// The [`TypeId`] must not already be registered as a resource.
unsafe fn force_register_arbitrary_resource( unsafe fn register_arbitrary_resource(
&self, &self,
type_id: TypeId, type_id: TypeId,
descriptor: ComponentDescriptor, descriptor: ComponentDescriptor,
func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static, func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static,
) -> ComponentId { ) -> ComponentId {
let id = self.ids.next();
self.components self.components
.queued .queued
.write() .write()
.unwrap_or_else(PoisonError::into_inner) .unwrap_or_else(PoisonError::into_inner)
.resources .resources
.insert( .entry(type_id)
type_id, .or_insert_with(|| {
// SAFETY: The id was just generated. // SAFETY: The id was just generated.
unsafe { QueuedRegistration::new(id, descriptor, func) }, unsafe { QueuedRegistration::new(self.ids.next(), descriptor, func) }
); })
id .id
} }
/// Queues this function to run as a dynamic registrator. /// Queues this function to run as a dynamic registrator.
fn force_register_arbitrary_dynamic( fn register_arbitrary_dynamic(
&self, &self,
descriptor: ComponentDescriptor, descriptor: ComponentDescriptor,
func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static, func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static,
@ -554,9 +554,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
#[inline] #[inline]
pub fn queue_register_component<T: Component>(&self) -> ComponentId { pub fn queue_register_component<T: Component>(&self) -> ComponentId {
self.component_id::<T>().unwrap_or_else(|| { self.component_id::<T>().unwrap_or_else(|| {
// SAFETY: We just checked that this type was not in the queue. // SAFETY: We just checked that this type was not already registered.
unsafe { unsafe {
self.force_register_arbitrary_component( self.register_arbitrary_component(
TypeId::of::<T>(), TypeId::of::<T>(),
ComponentDescriptor::new::<T>(), ComponentDescriptor::new::<T>(),
|registrator, id, _descriptor| { |registrator, id, _descriptor| {
@ -584,7 +584,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
&self, &self,
descriptor: ComponentDescriptor, descriptor: ComponentDescriptor,
) -> ComponentId { ) -> ComponentId {
self.force_register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| { self.register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| {
// SAFETY: Id uniqueness handled by caller. // SAFETY: Id uniqueness handled by caller.
unsafe { unsafe {
registrator.register_component_inner(id, descriptor); registrator.register_component_inner(id, descriptor);
@ -606,9 +606,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
pub fn queue_register_resource<T: Resource>(&self) -> ComponentId { pub fn queue_register_resource<T: Resource>(&self) -> ComponentId {
let type_id = TypeId::of::<T>(); let type_id = TypeId::of::<T>();
self.get_resource_id(type_id).unwrap_or_else(|| { self.get_resource_id(type_id).unwrap_or_else(|| {
// SAFETY: We just checked that this type was not in the queue. // SAFETY: We just checked that this type was not already registered.
unsafe { unsafe {
self.force_register_arbitrary_resource( self.register_arbitrary_resource(
type_id, type_id,
ComponentDescriptor::new_resource::<T>(), ComponentDescriptor::new_resource::<T>(),
move |registrator, id, descriptor| { move |registrator, id, descriptor| {
@ -638,9 +638,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
pub fn queue_register_non_send<T: Any>(&self) -> ComponentId { pub fn queue_register_non_send<T: Any>(&self) -> ComponentId {
let type_id = TypeId::of::<T>(); let type_id = TypeId::of::<T>();
self.get_resource_id(type_id).unwrap_or_else(|| { self.get_resource_id(type_id).unwrap_or_else(|| {
// SAFETY: We just checked that this type was not in the queue. // SAFETY: We just checked that this type was not already registered.
unsafe { unsafe {
self.force_register_arbitrary_resource( self.register_arbitrary_resource(
type_id, type_id,
ComponentDescriptor::new_non_send::<T>(StorageType::default()), ComponentDescriptor::new_non_send::<T>(StorageType::default()),
move |registrator, id, descriptor| { move |registrator, id, descriptor| {
@ -669,7 +669,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> {
&self, &self,
descriptor: ComponentDescriptor, descriptor: ComponentDescriptor,
) -> ComponentId { ) -> ComponentId {
self.force_register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| { self.register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| {
// SAFETY: Id uniqueness handled by caller. // SAFETY: Id uniqueness handled by caller.
unsafe { unsafe {
registrator.register_component_inner(id, descriptor); registrator.register_component_inner(id, descriptor);

View File

@ -2776,4 +2776,17 @@ mod tests {
fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {} fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {}
} }
#[test]
fn queue_register_component_toctou() {
for _ in 0..1000 {
let w = World::new();
std::thread::scope(|s| {
let c1 = s.spawn(|| w.components_queue().queue_register_component::<A>());
let c2 = s.spawn(|| w.components_queue().queue_register_component::<A>());
assert_eq!(c1.join().unwrap(), c2.join().unwrap());
});
}
}
} }