Skip to content

Reduce the scope of unsafe and ways to cause UB #13257

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

Open
wants to merge 8 commits into
base: dev
Choose a base branch
from

Conversation

sftse
Copy link
Contributor

@sftse sftse commented Apr 17, 2025

This reduces the number of public API functions that can have bad interactions and cause UB.

This reduces it to only fn inner() and fn unmanage() while previously any combination of methods on StateManager were potentially perilous.

@sftse sftse requested a review from a team as a code owner April 17, 2025 15:36
@github-project-automation github-project-automation bot moved this to 📬Proposal in Roadmap Apr 17, 2025
Copy link
Contributor

github-actions bot commented Apr 17, 2025

Package Changes Through a472d51

There are 8 changes which include tauri-bundler with minor, tauri with minor, tauri-cli with minor, tauri-codegen with minor, tauri-utils with minor, @tauri-apps/api with minor, @tauri-apps/cli with minor, tauri-runtime-wry with patch

Planned Package Versions

The following package releases are the planned based on the context of changes in this pull request.

package current next
@tauri-apps/api 2.5.0 2.6.0
tauri-utils 2.4.0 2.5.0
tauri-bundler 2.4.0 2.5.0
tauri-runtime 2.6.0 2.6.1
tauri-runtime-wry 2.6.0 2.6.1
tauri-codegen 2.2.0 2.3.0
tauri-macros 2.2.0 2.2.1
tauri-plugin 2.2.0 2.2.1
tauri-build 2.2.0 2.2.1
tauri 2.5.1 2.6.0
@tauri-apps/cli 2.5.0 2.6.0
tauri-cli 2.5.0 2.6.0

Add another change file through the GitHub UI by following this link.


Read about change files or the docs at github.com/jbolda/covector

Copy link
Contributor

@Legend-Master Legend-Master left a comment

Choose a reason for hiding this comment

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

I think originally, the life time alone was enough since the state will be staying for as long as the StateManager, but later on, with the introduction of unmanage, this is no longer true, so now we need to wrap it inside an Arc (#12745 or what this PR tries to do)

Personally, I agree with @WSH032 on this one, and I would probably think we should remove this function in v3

I personally believe that most people do not need the unmanage feature (it seems that tauri itself does not use it also); if users really need unmanage, they can implement it themselves using Mutex and Option::take. This way, users who do not need mutability (unmanage) do not have to pay the performance cost (use Arc instead of Box, and Arc::clone for every get).
https://github.com/tauri-apps/tauri/pull/12745/files#r1962817326

For now though, I think this is fine either ways (a safe implementation for unmanage with a higher overhead vs the current unsafe implementation), just some suggestions on the implementation in this PR:

diff --git a/crates/tauri/src/state.rs b/crates/tauri/src/state.rs
index 37adc8464..69a03afe1 100644
--- a/crates/tauri/src/state.rs
+++ b/crates/tauri/src/state.rs
@@ -6,7 +6,6 @@ use std::{
   any::{Any, TypeId},
   collections::HashMap,
   hash::BuildHasherDefault,
-  marker::PhantomData,
   sync::Arc,
   sync::Mutex,
 };
@@ -20,10 +19,7 @@ use crate::{
 ///
 /// See [`Manager::manage`](`crate::Manager::manage`) for usage examples.
 #[derive(Clone)]
-pub struct State<'r, T: Send + Sync + 'static> {
-  _life: PhantomData<&'r T>,
-  t: Arc<dyn Any + Send + Sync>,
-}
+pub struct State<'r, T: Send + Sync + 'static>(Arc<&'r T>);
 
 impl<'r, T: Send + Sync + 'static> State<'r, T> {
   /// Retrieve a borrow to the underlying value with a lifetime of `'r`.
@@ -31,13 +27,7 @@ impl<'r, T: Send + Sync + 'static> State<'r, T> {
   /// [`std::ops::Deref`] with a [`std::ops::Deref::Target`] of `T`.
   #[inline(always)]
   pub fn inner(&self) -> &'r T {
-    let x: &(dyn Any + Send + Sync) = &*self.t;
-    // SAFETY: everything returned from this function must no longer live when fn unmanage() is called
-    unsafe {
-      &*(x
-        .downcast_ref::<T>()
-        .expect("the type of the key should be same as the type of the value") as *const T)
-    }
+    *self.0
   }
 }
 
@@ -46,19 +36,19 @@ impl<T: Send + Sync + 'static> std::ops::Deref for State<'_, T> {
 
   #[inline(always)]
   fn deref(&self) -> &T {
-    self.inner()
+    &self.0
   }
 }
 
 impl<T: Send + Sync + 'static + PartialEq> PartialEq for State<'_, T> {
   fn eq(&self, other: &Self) -> bool {
-    self.t.downcast_ref::<T>() == other.t.downcast_ref::<T>()
+    self.0 == other.0
   }
 }
 
 impl<T: Send + Sync + std::fmt::Debug> std::fmt::Debug for State<'_, T> {
   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-    f.debug_tuple("State").field(&self.t).finish()
+    f.debug_tuple("State").field(&self.0).finish()
   }
 }
 
@@ -154,10 +144,11 @@ impl StateManager {
     let map = self.map.lock().unwrap();
     let type_id = TypeId::of::<T>();
     let state = map.get(&type_id)?;
-    Some(State {
-      _life: PhantomData,
-      t: state.clone(),
-    })
+    Some(State(
+      Arc::downcast::<&T>(state.clone())
+        // SAFETY: the type of the key is the same as the type of the value
+        .expect("the type of the key should be same as the type of the value"),
+    ))
   }
 }

also pinging @amrbashir since you did most of the reviews on the last one

@WSH032
Copy link
Contributor

WSH032 commented Jun 1, 2025

I noticed that many pieces of code/plugins frequently call manager::state, so personally, I care more about the performance aspect. (At least, I think Pin provides the same safety semantics as Arc 🤓)

@sftse
Copy link
Contributor Author

sftse commented Jun 2, 2025

@@ -154,10 +144,11 @@ impl StateManager {
     let map = self.map.lock().unwrap();
     let type_id = TypeId::of::<T>();
     let state = map.get(&type_id)?;
-    Some(State {
-      _life: PhantomData,
-      t: state.clone(),
-    })
+    Some(State(
+      Arc::downcast::<&T>(state.clone())
+        // SAFETY: the type of the key is the same as the type of the value
+        .expect("the type of the key should be same as the type of the value"),
+    ))
   }
 }

This downcast will fail, the type when it was inserted is T but this attempts to downcast to &T.

let mut map: HashMap<TypeId, Arc<dyn Any + Send + Sync>> = HashMap::new();
map.insert(TypeId::of::<u8>(), Arc::new(0) as Arc<_>);
let arc = map.get(&TypeId::of::<u8>()).unwrap();
assert!(Arc::downcast::<&u8>(arc.clone()).is_err());

There is a way to get this to work with these types, but it defeats the purpose of this PR. Creating the &'r T in fn try_get() means any combination of try_get+unmanage or get+unmanage (and by extension inner+unmanage because inner is a method on the return type of try_get or get) is potentially unsound. The Arc delays creating the reference until the call to inner, meaning if either inner or unmanage are never called, there's no unsoundness.

However, it is a bit contorted, if only to reduce the scope of UB without changing the public API. I don't think the Arc<&'r T> has any benefits over the previous code simply containing &'r T.

I noticed that many pieces of code/plugins frequently call manager::state, so personally, I care more about the performance aspect. (At least, I think Pin provides the same safety semantics as Arc 🤓)

I removed the Pin and the unsafe block because an unsafe block means the safety contract is upheld, but fn unmanage does move the value behind the Pin, violating the contract. This reads as if fn unmanage is unconditionally unsound.

@Legend-Master
Copy link
Contributor

You're right, ignore my suggestions then, I thought the downcast would work but apparently no

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 📬Proposal
Development

Successfully merging this pull request may close these issues.

3 participants