Skip to content
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

Rework the safety invariants of Global a bit #750

Merged
merged 1 commit into from
Jun 10, 2024

Conversation

lilizoey
Copy link
Member

@lilizoey lilizoey commented Jun 9, 2024

Mainly moves the burden of proving that the InitState is Initialized to when the GlobalGuard is constructed. This includes a debug_assert that the InitState is in the correct state, which meant some other debug_asserts that did the same thing could be removed. I think this could be made entirely safe once mapped mutex guards stabilize.

Also removes TransientInitializing entirely since it was just used to get the init_fn out of the InitState::Pending. However function pointers are Copy so we dont need to replace it with anything, and we have the InitState locked so it would never be observable anyway.

@lilizoey lilizoey added quality-of-life No new functionality, but improves ergonomics/internals c: core Core components labels Jun 9, 2024
Copy link
Member

@Bromeon Bromeon left a comment

Choose a reason for hiding this comment

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

Thank you!

Previously it was relatively easy to add #![forbid(unsafe)] by just replacing unreachable_unchecked with unreachable!. Could this still technically be done, if we ever wanted to (e.g. new instead of new_unchecked, unwrap instead of unwrap_unchecked)?

pub struct GlobalGuard<'a, T> {
mutex_guard: MutexGuard<'a, InitState<T>>,
}
mod global_guard {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this extra module is necessary, the separator is enough 🙂

Copy link
Member Author

Choose a reason for hiding this comment

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

the extra module is there to ensure the mutex_guard field isn't accessible outside of a very limited set of functions. of course it isn't technically necessary, but by doing this we keep the safety critical logic more local since anything that wants to construct the GlobalGuard must go through either new or new_unchecked.

Copy link
Member

Choose a reason for hiding this comment

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

I see, OK. Could you add this as a comment before mod?
Something like // Encapsulate private fields. or so is enough.

Comment on lines 207 to 212
fn initialized(&self) -> Option<&T> {
match self {
InitState::Initialized(t) => t,
_ => {
// SAFETY: This method is only called from a guard, which can only be obtained in Initialized state.
unsafe { std::hint::unreachable_unchecked() }
}
InitState::Initialized(t) => Some(t),
_ => None,
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Nice change, as it moves the entire unsafety to the caller and doesn't make assumptions about where it's called from 👍

Name also makes sense, maybe as_initialized[_mut] would be even clearer, to indicate the reference return?

Comment on lines 189 to 191
Poisoned {
circumvent: Option<GlobalGuard<'a, T>>,
},
Copy link
Member

Choose a reason for hiding this comment

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

This weakens the API type safety towards the user -- I don't think there's a case where this can be None?

Copy link
Member Author

Choose a reason for hiding this comment

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

i dont think so, but it is a bit hard to be 100% confident about it. The only time it can be None is if there is a panic when the InitState mutex is locked, but hasn't been set to Initialized yet, and also isn't caught by the catch_unwind. And while i dont think that can happen, it's rather involved control flow and i havent been able to fully convince myself that it definitely wont happen.

Another note is that it's also not actually accessed anywhere. the only place the Poisoned variant is used it also just discards the circumvent value.

Another option than using new_unchecked() would be new().expect() which makes it not (library) UB to create the GlobalGuard, but does throw a panic. However if we are pretty certain that this wont happen, and this error case is fairly rare anyway, then maybe that's just fine?.

Copy link
Member

@Bromeon Bromeon Jun 10, 2024

Choose a reason for hiding this comment

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

Generally I'd tend to make it as expressive as possible (i.e. not Option), and if our assumptions end up being wrong, treat it as a bug to be fixed.

I think new().expect() would be the safe choice here, retrieving poisoned guards shouldn't be the hot path. If it turns out that we limit an actual use case without None, then we'll have to make a (breaking) API change.

@lilizoey
Copy link
Member Author

Thank you!

Previously it was relatively easy to add #![forbid(unsafe)] by just replacing unreachable_unchecked with unreachable!. Could this still technically be done, if we ever wanted to (e.g. new instead of new_unchecked, unwrap instead of unwrap_unchecked)?

It's still relatively easy, the changes needed now would be:

  1. remove new_unchecked
  2. change the unwrap_unchecked() to unwrap()
  3. replace all calls to new_unchecked() with new().unwrap()

@lilizoey lilizoey force-pushed the refactor/global-safety branch from 98b2e22 to 50831ab Compare June 10, 2024 16:14
@lilizoey lilizoey added this pull request to the merge queue Jun 10, 2024
Merged via the queue into godot-rust:master with commit 7eec09c Jun 10, 2024
15 checks passed
@lilizoey lilizoey deleted the refactor/global-safety branch June 10, 2024 16:35
///
/// The value must be `Initialized`.
pub(super) unsafe fn new_unchecked(mutex_guard: MutexGuard<'a, InitState<T>>) -> Self {
debug_assert!(matches!(*mutex_guard, InitState::Initialized(_)));
Copy link
Contributor

Choose a reason for hiding this comment

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

this isnt necessary as unwrap_unchecked panics in debug already.

Copy link
Member Author

Choose a reason for hiding this comment

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

the error message is different, and this also more explicitly lays out what the reconditioned is

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components quality-of-life No new functionality, but improves ergonomics/internals
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants