Skip to content

Fix unsoundness in QueryIter::sort_by #17826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 26, 2025

Conversation

chescock
Copy link
Contributor

@chescock chescock commented Feb 12, 2025

Objective

QueryIter::sort_by() is unsound. It passes the lens items with the full 'w lifetime, and a malicious user could smuggle them out of the closure where they could alias with the query results.

Solution

Make the sort closures generic in the lifetime parameter of the lens item. This ensures the lens items cannot outlive the call to the closure.

Testing

Added a compile-fail test that demonstrates the unsound pattern.

Migration Guide

The sort family of methods on QueryIter unsoundly gave access L::Item<'w> with the full 'w lifetime. It has been shortened to L::Item<'w> so that items cannot escape the comparer. If you get lifetime errors using these methods, you will need to make the comparer generic in the new lifetime. Often this can be done by replacing named 'w with '_, or by replacing the use of a function item with a closure.

// Before: Now fails with "error: implementation of `FnMut` is not general enough"
query.iter().sort_by::<&C>(Ord::cmp);
// After: Wrap in a closure
query.iter().sort_by::<&C>(|l, r| Ord::cmp(l, r));

query.iter().sort_by::<&C>(comparer);
// Before: Uses specific `'w` lifetime from some outer scope
// now fails with "error: implementation of `FnMut` is not general enough"
fn comparer(left: &&'w C, right: &&'w C) -> Ordering { /* ... */ }
// After: Accepts any lifetime using inferred lifetime parameter
fn comparer(left: &&C, right: &&C) -> Ordering { /* ... */ }

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-ECS Entities, components, systems, and events P-Unsound A bug that results in undefined compiler behavior D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Uncontroversial This work is generally agreed upon labels Feb 12, 2025
@alice-i-cecile
Copy link
Member

Core idea is good, but CI is pointing out a few related problems I think.

@alice-i-cecile alice-i-cecile added this to the 0.16 milestone Feb 12, 2025
@@ -716,7 +722,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
/// .sort_by_key::<EntityRef, _>(|entity_ref| {
/// (
/// entity_ref.contains::<AvailableMarker>(),
/// entity_ref.get::<Rarity>()
/// entity_ref.get::<Rarity>().copied()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A little bit of collateral damage here: sort_by_key can no longer borrow from the lens item. It's theoretically possible to support that, but it's hard to express with Fn traits.

It's still possible to express those sorts using sort_by, so that doesn't make anything impossible, just inconvenient.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is likely just the original Rust limitation of sort_by_key/sort_by_cached_key, the closure can only return Copy or unusually long-lived data.

@Victoronz
Copy link
Contributor

Victoronz commented Feb 12, 2025

Hmm, was this a problem before? I see an iter_many_inner in sort_impl, this should just be iter_many. This was introduced by #15858.
Are these changes still necessary when changing this iter_many to a non-consuming iteration?

@chescock
Copy link
Contributor Author

Hmm, was this a problem before? I see an iter_many_inner in sort_impl, this should just be iter_many. This was introduced by #15858.
Are these changes still necessary when changing this iter_many to a non-consuming iteration?

Yup, it reproduces before #15858. That PR only changed the implementation of the sort methods, and the compile-fail test here only relies on the signature.

@@ -2898,13 +2904,13 @@ mod tests {
{
let mut query = query_state
.iter_many_mut(&mut world, [id, id])
.sort_by::<&C>(Ord::cmp);
.sort_by::<&C>(|l, r| Ord::cmp(l, r));
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this fail if just the function pointer is passed?
Unlike with sort_by_key, slice::sort_by is able to accept these without the need to wrap it in a closure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It failed CI with error: implementation of `FnMut` is not general enough:
https://github.com/bevyengine/bevy/actions/runs/13291587519/job/37113490763#step:6:119

I admit I didn't investigate it too carefully; the rules for when rust infers higher-ranked lifetimes versus specific ones seem very finnicky to me.

Copy link
Contributor

@hymm hymm Feb 14, 2025

Choose a reason for hiding this comment

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

found a description of the problem here https://users.rust-lang.org/t/problem-with-lifetimes-in-closure-return-types/79026

TLDR: the lifetime of Ord::cmp has a specific Item<'a> lifetime, while sort_by expects 'a to be hrtb. Doesn't seem fixable without tradeoffs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I bet it'll infer HRTB over lifetime parameters to the function but not to lifetime parameters to the trait or type. That's reasonable, even if I wish it were looser. I remember hitting some of the odd cases mentioned in https://rust-lang.github.io/rfcs/3216-closure-lifetime-binder.html, though, which left me with the impression that higher-ranked closure inference was always a little arbitrary.

Copy link
Contributor

@Victoronz Victoronz left a comment

Choose a reason for hiding this comment

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

While the ergonomics regressions hurt a bit, soundness is soundness.

@Victoronz
Copy link
Contributor

Also, this should probably have a migration guide.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Feb 26, 2025
Copy link
Contributor

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Could you add some context on what users should update when this change get released in a new version of Bevy?
It will be used to help writing the migration guide for the version. Putting it after a ## Migration Guide will help it get automatically picked up by our tooling.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Feb 26, 2025
Merged via the queue into bevyengine:main with commit 94e6fa1 Feb 26, 2025
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Bug An unexpected or incorrect behavior D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide P-Unsound A bug that results in undefined compiler behavior S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants